diff --git a/.claude/skills/kotlin-expert/references/common-utilities.md b/.claude/skills/kotlin-expert/references/common-utilities.md index c2e0de633e..138cc44312 100644 --- a/.claude/skills/kotlin-expert/references/common-utilities.md +++ b/.claude/skills/kotlin-expert/references/common-utilities.md @@ -47,7 +47,6 @@ Under `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/`: - **`commons/src/commonMain/.../threading/Threading.kt`** — shared dispatchers and `CoroutineScope` helpers for commonMain code. - **`amethyst/src/main/java/.../service/CoroutinesExt.kt`** — Android-only helpers: `launchIO(block)`, `launchMain(block)` built on top of `Dispatchers.IO` / `Dispatchers.Main`. Use these in ViewModels and services to stop re-spelling the dispatcher every time. -- **`amethyst/src/main/java/.../service/MainThreadChecker.kt`** — debug assertion helper for catching main-thread misuse during dev. ## Quartz iterables & JSON diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/service/uploads/AvifMetadataStripperPoisonedFixtureInstrumentedTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/service/uploads/AvifMetadataStripperPoisonedFixtureInstrumentedTest.kt index a2f3b19e03..f69bbe1f4c 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/service/uploads/AvifMetadataStripperPoisonedFixtureInstrumentedTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/service/uploads/AvifMetadataStripperPoisonedFixtureInstrumentedTest.kt @@ -24,6 +24,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.amethyst.AvifInstrumentedTestSupport.appContext import com.vitorpamplona.amethyst.AvifInstrumentedTestSupport.contentUriFor import com.vitorpamplona.amethyst.AvifInstrumentedTestSupport.copyAssetToCache +import com.vitorpamplona.amethyst.commons.service.uploads.AVIF_MIME import org.junit.Assert.assertNotNull import org.junit.Assert.assertThrows import org.junit.Test diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/service/uploads/AvifUploadPipelineInstrumentedTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/service/uploads/AvifUploadPipelineInstrumentedTest.kt index 983d217776..44ae559b7a 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/service/uploads/AvifUploadPipelineInstrumentedTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/service/uploads/AvifUploadPipelineInstrumentedTest.kt @@ -26,6 +26,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.amethyst.AvifInstrumentedTestSupport.appContext import com.vitorpamplona.amethyst.AvifInstrumentedTestSupport.contentUriFor import com.vitorpamplona.amethyst.AvifInstrumentedTestSupport.copyAssetToCache +import com.vitorpamplona.amethyst.commons.service.uploads.AVIF_MIME +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull 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 114e9c5ee7..bc5aab376e 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 @@ -71,9 +71,9 @@ import com.vitorpamplona.amethyst.commons.resources.select_push_server import com.vitorpamplona.amethyst.commons.ui.components.SpinnerSelectionDialog import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsBlockTile import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.service.notifications.PushDistributorHandler -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsBlockTile import com.vitorpamplona.quartz.utils.Log import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AmethystLocalCacheHost.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AmethystLocalCacheHost.kt index a0c3ec1830..7273c65a6b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AmethystLocalCacheHost.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AmethystLocalCacheHost.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.FileSystemNip95BlobStore import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.LocalCacheHost import com.vitorpamplona.amethyst.commons.model.cache.Nip95BlobStore -import com.vitorpamplona.amethyst.service.checkNotInMainThread +import com.vitorpamplona.amethyst.commons.service.UsageCounter import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -55,6 +55,8 @@ class AmethystLocalCacheHost( override val relayStats: RelayStats get() = modules.relayStats + override val usageCounter: UsageCounter = UsageCounter { key, amount -> modules.resourceUsage.add(key, amount) } + override fun relaySelfPubKey(relay: NormalizedRelayUrl): HexKey? = modules.nip11Cache.getFromCache(relay).self override fun relayInfo(relay: NormalizedRelayUrl): Nip11RelayInformation = modules.nip11Cache.getFromCache(relay) @@ -63,6 +65,4 @@ class AmethystLocalCacheHost( // The zap path's LNURL cache is a quartz-side singleton the outbound-zap resolver fills. override fun lnurlEndpoint(lnurlpUrl: String): LnurlEndpointInfo? = LnurlEndpointCache.get(lnurlpUrl) - - override fun assertNotMainThread() = checkNotInMainThread() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt index c68b4cfb12..982ac58955 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt @@ -80,6 +80,7 @@ import com.vitorpamplona.amethyst.commons.service.lnurl.OkHttpLnurlEndpointResol import com.vitorpamplona.amethyst.commons.service.pow.PoWJobStore import com.vitorpamplona.amethyst.commons.service.pow.PoWPolicy import com.vitorpamplona.amethyst.commons.service.pow.PoWPublishQueue +import com.vitorpamplona.amethyst.commons.service.resourceusage.UsageKeys import com.vitorpamplona.amethyst.commons.state.UiSettingsState import com.vitorpamplona.amethyst.commons.tor.TorRelayState import com.vitorpamplona.amethyst.commons.tor.TorSettings @@ -136,7 +137,6 @@ import com.vitorpamplona.amethyst.service.resourceusage.ResourceUsageStore import com.vitorpamplona.amethyst.service.resourceusage.ScreenTimeIntegrator import com.vitorpamplona.amethyst.service.resourceusage.SessionTimeIntegrator import com.vitorpamplona.amethyst.service.resourceusage.UsageCountingInterceptor -import com.vitorpamplona.amethyst.service.resourceusage.UsageKeys import com.vitorpamplona.amethyst.service.safeCacheDir import com.vitorpamplona.amethyst.service.scheduledposts.ScheduledPostWorker import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomMirrorQueue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index fc03aa2f99..7bd70be7aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst -import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences import androidx.compose.runtime.Immutable @@ -64,7 +63,6 @@ import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthPolicy import com.vitorpamplona.amethyst.model.backups.BackupConflictStorage import com.vitorpamplona.amethyst.model.nip60Cashu.CashuPreferences import com.vitorpamplona.amethyst.model.preferences.UiSharedPreferences -import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEvent import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListFragmentEvent import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent @@ -675,17 +673,14 @@ object LocalPreferences { } } - private fun encryptedPreferences(npub: String? = null): SharedPreferences { - checkNotInMainThread() - - return if (BuildConfig.DEBUG && DEBUG_PLAINTEXT_PREFERENCES) { + private fun encryptedPreferences(npub: String? = null): SharedPreferences = + if (BuildConfig.DEBUG && DEBUG_PLAINTEXT_PREFERENCES) { val preferenceFile = if (npub == null) DEBUG_PREFERENCES_NAME else "${DEBUG_PREFERENCES_NAME}_$npub" Amethyst.instance.appContext.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE) } else { Amethyst.instance.encryptedStorage(npub) } - } /** * Clears the preferences for a given npub, deletes the preferences xml file, and switches the @@ -695,7 +690,7 @@ object LocalPreferences { * deleted. If we use `apply()` there is a race condition and the file will probably not be * deleted */ - @SuppressLint("ApplySharedPref") + @Suppress("ApplySharedPref") suspend fun deleteAccount(accountInfo: AccountInfo) { Log.d("LocalPreferences") { "Saving to encrypted storage updatePrefsForLogout ${accountInfo.npub}" } withContext(Dispatchers.IO) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt index 45cfd84f05..6274bda285 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteAppLauncher.kt @@ -27,6 +27,7 @@ import android.os.Bundle import android.widget.Toast import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp +import com.vitorpamplona.amethyst.commons.favorites.favoriteCoordinateOf import com.vitorpamplona.amethyst.commons.model.ThemeType import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.napplet.NappletLauncher @@ -212,17 +213,6 @@ object FavoriteAppLauncher { } } - /** - * The addressable coordinate `kind:pubkey:dtag` used to key an nsite/napplet favorite. Stored - * instead of the content hash so the favorite survives routine code/manifest updates. - */ - fun coordinateOf(event: Event): String { - val dTag = - when (event) { - is NamedNappletEvent -> event.identifier() - is NamedSiteEvent -> event.identifier() - else -> "" - } - return "${event.kind}:${event.pubKey}:$dTag" - } + /** See [favoriteCoordinateOf]. */ + fun coordinateOf(event: Event): String = favoriteCoordinateOf(event) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/RoleBasedHttpClientBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/RoleBasedHttpClientBuilder.kt index 278443ed5e..69d943517d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/RoleBasedHttpClientBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/RoleBasedHttpClientBuilder.kt @@ -23,10 +23,10 @@ package com.vitorpamplona.amethyst.model.privacyOptions import com.vitorpamplona.amethyst.commons.service.http.DualHttpClientManager import com.vitorpamplona.amethyst.commons.service.http.IRoleBasedHttpClientBuilder import com.vitorpamplona.amethyst.commons.service.http.ProxiedSocketFactory +import com.vitorpamplona.amethyst.commons.service.resourceusage.UsageKeys import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow import com.vitorpamplona.amethyst.commons.tor.TorType import com.vitorpamplona.amethyst.service.resourceusage.HttpUsageMeter -import com.vitorpamplona.amethyst.service.resourceusage.UsageKeys import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import okhttp3.OkHttpClient import java.net.InetSocketAddress diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt index d5b822e42c..f091ebd073 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt @@ -487,7 +487,7 @@ class NappletBrokerService : Service() { /** * Brings the main activity (a `singleInstance`) forward at the Connected Apps detail for [coordinate], - * via the in-process `connectedapp?coordinate=` deep link that [com.vitorpamplona.amethyst.ui.uriToRoute] + * via the in-process `connectedapp?coordinate=` deep link that [com.vitorpamplona.amethyst.commons.ui.uriToRoute] * resolves. Used when a full-screen sandbox surface taps "Manage permissions". */ private fun openConnectedAppDetail(coordinate: String) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/PackageUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/PackageUtils.kt index a25b3e85d0..70ef33c162 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/PackageUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/PackageUtils.kt @@ -20,11 +20,10 @@ */ package com.vitorpamplona.amethyst.service -import android.annotation.SuppressLint import android.content.Context object PackageUtils { - @SuppressLint("QueryPermissionsNeeded") + @Suppress("QueryPermissionsNeeded") private fun isPackageInstalled( context: Context, target: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/calendar/CalendarReminderWorker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/calendar/CalendarReminderWorker.kt index b3f34ae3ce..06dab13025 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/calendar/CalendarReminderWorker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/calendar/CalendarReminderWorker.kt @@ -32,9 +32,9 @@ import com.vitorpamplona.amethyst.commons.model.nip52Calendar.appointmentView import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.calendar_reminder_body import com.vitorpamplona.amethyst.commons.resources.calendar_reminder_default_title +import com.vitorpamplona.amethyst.commons.service.resourceusage.UsageKeys import com.vitorpamplona.amethyst.commons.ui.loadPluralStringRes import com.vitorpamplona.amethyst.commons.ui.loadStringRes -import com.vitorpamplona.amethyst.service.resourceusage.UsageKeys import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.tags.RSVPStatusTag import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CashuParser.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CashuParser.kt index 42f09e77dd..55ef6e04b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CashuParser.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CashuParser.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.service.cashu import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable -import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.nip60Cashu.token.CashuToken import com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenB64Parser import kotlinx.collections.immutable.ImmutableList @@ -34,8 +33,6 @@ import kotlinx.collections.immutable.toImmutableList */ class CashuParser { fun parse(cashuToken: String): GenericLoadable> { - checkNotInMainThread() - val parsed = CashuTokenB64Parser.parse(cashuToken) ?: return GenericLoadable.Error("Could not parse this cashu token") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationFlow.kt index 0fd483cf2a..171183e9a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationFlow.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.service.location -import android.annotation.SuppressLint import android.location.Location import android.location.LocationListener import android.location.LocationManager @@ -67,7 +66,7 @@ class LocationFlow( private val locationManager: LocationManager, private val sdkInt: Int = Build.VERSION.SDK_INT, ) { - @SuppressLint("MissingPermission") + @Suppress("MissingPermission") fun get( minTimeMs: Long, minDistanceM: Float, @@ -114,7 +113,7 @@ class LocationFlow( } /** True when the registration was accepted; false when the provider refused it. */ - @SuppressLint("MissingPermission") + @Suppress("MissingPermission") private fun requestUpdates( provider: String, minTimeMs: Long, @@ -143,7 +142,7 @@ class LocationFlow( * the update request is — on a device where one refuses us, the others * should still seed. */ - @SuppressLint("MissingPermission") + @Suppress("MissingPermission") private fun freshestLastKnownLocation(providers: List): Location? = providers .mapNotNull { provider -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/logging/LogMonitor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/logging/LogMonitor.kt index 21fddb7c07..b9e4531bd5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/logging/LogMonitor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/logging/LogMonitor.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.service.logging -import android.annotation.SuppressLint import android.os.Handler import android.os.HandlerThread import android.os.Looper @@ -166,7 +165,7 @@ class StackSampler( companion object { const val SEPARATOR: String = "\r\n" - @SuppressLint("SimpleDateFormat") + @Suppress("SimpleDateFormat") val TIME_FORMATTER: SimpleDateFormat = SimpleDateFormat("MM-dd HH:mm:ss.SSS") } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationCatchUpWorker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationCatchUpWorker.kt index d11a88bd00..ed54f71552 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationCatchUpWorker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationCatchUpWorker.kt @@ -31,7 +31,7 @@ import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import androidx.work.WorkerParameters import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.service.resourceusage.UsageKeys +import com.vitorpamplona.amethyst.commons.service.resourceusage.UsageKeys import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/payments/PayToAppAvailability.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/payments/PayToAppAvailability.kt index 8657b94992..e52ed073da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/payments/PayToAppAvailability.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/payments/PayToAppAvailability.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.service.payments -import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.content.pm.PackageManager @@ -196,7 +195,7 @@ object PayToAppAvailability { }.getOrNull() } - @SuppressLint("QueryPermissionsNeeded") + @Suppress("QueryPermissionsNeeded") private fun queryActivities( pm: PackageManager, intent: Intent, @@ -208,7 +207,7 @@ object PayToAppAvailability { }.getOrDefault(emptyList()) /** Packages that answer a URL nobody can own — i.e. general-purpose browsers. */ - @SuppressLint("QueryPermissionsNeeded") + @Suppress("QueryPermissionsNeeded") private fun browserPackages(pm: PackageManager): Set = queryActivities(pm, viewIntent(CONTROL_URL)).mapNotNull { it.packageName() }.toSet() /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/FakeWaveformAnimation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/FakeWaveformAnimation.kt index 1443c3ab45..f14983fde2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/FakeWaveformAnimation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/FakeWaveformAnimation.kt @@ -20,29 +20,15 @@ */ package com.vitorpamplona.amethyst.service.playback.composable.wavefront -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredHeight -import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableFloatState import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.Fill -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import androidx.media3.common.Player -import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn +import com.vitorpamplona.amethyst.commons.ui.components.wavefront.FakeWaveformAnimation import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -50,7 +36,6 @@ import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onStart -import kotlin.math.sin @Composable fun FakeWaveformAnimation( @@ -97,60 +82,3 @@ fun pollCurrentPosition(controller: Player) = emit(controller.currentPosition) }.flowOn(Dispatchers.IO) .conflate() - -@Preview -@Composable -fun FakeWaveformAnimationPreview() { - val state = - remember { - mutableFloatStateOf((500 % 1000.0f) / 1000.0f) - } - ThemeComparisonColumn { - FakeWaveformAnimation(state, 50, Modifier.width(200.dp)) - } -} - -@Composable -fun FakeWaveformAnimation( - waveformProgress: MutableFloatState, - barCount: Int = 50, - modifier: Modifier = Modifier, -) { - val barBrush = - Brush.linearGradient( - colors = listOf(Color(0xff2598cf), Color(0xff652d80)), - start = Offset(0.2f * 128f, 0f), - end = Offset(0.2f * 128f + 128f, 0f), - ) - - Canvas( - modifier = - modifier - .fillMaxWidth() - .requiredHeight(48.dp) - .padding(horizontal = 10.dp), - ) { - val barWidth = 3.dp.toPx() - val barPadding = 2.dp.toPx() - val barRadius = 2.dp.toPx() - val totalBarWidth = barWidth + barPadding - val startX = (size.width - (barCount * totalBarWidth - barPadding)) / 2f - - for (i in 0 until barCount) { - val phase = (i.toFloat() / barCount) * 2f * Math.PI.toFloat() - val wave = sin(waveformProgress.floatValue * 2f * Math.PI.toFloat() + phase) - val barFraction = ((wave + 1f) / 2f) - val barHeight = size.height * barFraction - val x = startX + i * totalBarWidth - val y = (size.height - barHeight) / 2f - - drawRoundRect( - brush = barBrush, - topLeft = Offset(x, y), - size = Size(barWidth, barHeight), - cornerRadius = CornerRadius(barRadius, barRadius), - style = Fill, - ) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCache.kt index 5f10d1a4fc..500e3a3db5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCache.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.service.playback.diskCache -import android.annotation.SuppressLint import android.content.Context import android.os.StatFs import androidx.media3.database.StandaloneDatabaseProvider @@ -34,7 +33,7 @@ import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache.Companio import com.vitorpamplona.quartz.utils.Log import java.io.File -@SuppressLint("UnsafeOptInUsageError") +@Suppress("UnsafeOptInUsageError") class VideoCache { companion object { // Target fraction of currently-available disk space. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaPlayTimeTracker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaPlayTimeTracker.kt index 44acb2d203..17e72de3c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaPlayTimeTracker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaPlayTimeTracker.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.service.playback.playerPool import android.os.SystemClock import androidx.media3.common.Player import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.service.resourceusage.UsageKeys +import com.vitorpamplona.amethyst.commons.service.resourceusage.UsageKeys /** * Accounts actual media playback time into the resource-usage ledger. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt index 583650c1f1..a3efa62313 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt @@ -21,10 +21,10 @@ package com.vitorpamplona.amethyst.service.relayClient import com.vitorpamplona.amethyst.commons.service.connectivity.ConnectivityStatus +import com.vitorpamplona.amethyst.commons.service.resourceusage.UsageKeys import com.vitorpamplona.amethyst.commons.tor.RelayClassification import com.vitorpamplona.amethyst.commons.tor.TorRelayEvaluation import com.vitorpamplona.amethyst.commons.tor.TorRelaySettings -import com.vitorpamplona.amethyst.service.resourceusage.UsageKeys import com.vitorpamplona.amethyst.ui.tor.TorServiceStatus import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.utils.Log diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthPromptHost.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthPromptHost.kt index 08759214ca..f6ec0e03b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthPromptHost.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthPromptHost.kt @@ -67,6 +67,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.commons.relayClient.auth.RelayAuthPrompt import com.vitorpamplona.amethyst.commons.relayClient.auth.UserAuthChoice +import com.vitorpamplona.amethyst.commons.relayClient.authCommand.compose.LoadRelayAuthUser import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserInfo import com.vitorpamplona.amethyst.commons.relayauth.AuthPurpose diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt index 8936bc37e8..e0b580ef74 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc -import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.commons.model.Note @@ -30,7 +29,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourc import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent -@SuppressLint("StateFlowValueCalledInComposition") +@Suppress("StateFlowValueCalledInComposition") @Composable fun NWCFinderFilterAssemblerSubscription( note: Note, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/BatteryDrainSampler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/BatteryDrainSampler.kt index 5487055071..f847ac2acc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/BatteryDrainSampler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/BatteryDrainSampler.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.amethyst.service.resourceusage +import com.vitorpamplona.amethyst.commons.service.resourceusage.UsageKeys + /** * Samples the device battery level at each ledger flush and accumulates the * drops that happen while discharging into [UsageKeys.BATTERY_DRAIN_FG] / diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/ForegroundTimeIntegrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/ForegroundTimeIntegrator.kt index 0e1bb3200c..c90b828f06 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/ForegroundTimeIntegrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/ForegroundTimeIntegrator.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.service.resourceusage import android.os.SystemClock +import com.vitorpamplona.amethyst.commons.service.resourceusage.UsageKeys import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/MeteringNostrSigner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/MeteringNostrSigner.kt index 8bfe2fe129..731d9a0532 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/MeteringNostrSigner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/MeteringNostrSigner.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service.resourceusage +import com.vitorpamplona.amethyst.commons.service.resourceusage.UsageKeys import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/ProcessCpuSampler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/ProcessCpuSampler.kt index 408101afa4..2c306bfb21 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/ProcessCpuSampler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/ProcessCpuSampler.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.service.resourceusage import android.os.Process +import com.vitorpamplona.amethyst.commons.service.resourceusage.UsageKeys /** * Samples whole-process CPU time (user+system, [Process.getElapsedCpuTime]) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/RelayConnectionTimeIntegrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/RelayConnectionTimeIntegrator.kt index b3afc9c6e5..23ebc58de7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/RelayConnectionTimeIntegrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/RelayConnectionTimeIntegrator.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.service.resourceusage import android.os.SystemClock +import com.vitorpamplona.amethyst.commons.service.resourceusage.UsageKeys import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/RelayUsageListener.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/RelayUsageListener.kt index 5abc1998e0..2b27b83e39 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/RelayUsageListener.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/RelayUsageListener.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.resourceusage import android.os.SystemClock import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.purposeOrNull +import com.vitorpamplona.amethyst.commons.service.resourceusage.UsageKeys import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayConnectionListener import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ClosedMessage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/ScreenTimeIntegrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/ScreenTimeIntegrator.kt index b995054ace..b072bb1d72 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/ScreenTimeIntegrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/ScreenTimeIntegrator.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.service.resourceusage import android.os.SystemClock +import com.vitorpamplona.amethyst.commons.service.resourceusage.UsageKeys import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/UsageCountingInterceptor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/UsageCountingInterceptor.kt index 2b2f9ea2cd..c1d3922967 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/UsageCountingInterceptor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/UsageCountingInterceptor.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.service.resourceusage import android.os.SystemClock +import com.vitorpamplona.amethyst.commons.service.resourceusage.UsageKeys import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Request diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/UsageSummary.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/UsageSummary.kt index 1fd24f085d..a00463ad45 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/UsageSummary.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/UsageSummary.kt @@ -20,7 +20,8 @@ */ package com.vitorpamplona.amethyst.service.resourceusage -import com.vitorpamplona.amethyst.service.resourceusage.UsageKeys.sumMatching +import com.vitorpamplona.amethyst.commons.service.resourceusage.UsageKeys +import com.vitorpamplona.amethyst.commons.service.resourceusage.UsageKeys.sumMatching /** * Headline metrics derived from one or more daily counter buckets. Shared by diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/scheduledposts/ScheduledPostWorker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/scheduledposts/ScheduledPostWorker.kt index aa2de5fc11..c34ca38dba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/scheduledposts/ScheduledPostWorker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/scheduledposts/ScheduledPostWorker.kt @@ -33,7 +33,7 @@ import androidx.work.WorkerParameters import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPostPublisher import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPostStatus -import com.vitorpamplona.amethyst.service.resourceusage.UsageKeys +import com.vitorpamplona.amethyst.commons.service.resourceusage.UsageKeys import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.publishAndConfirmDetailed import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt new file mode 100644 index 0000000000..2e670493a4 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/AndroidMediaUploader.kt @@ -0,0 +1,595 @@ +/* + * 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.service.uploads + +import android.content.Context +import android.net.Uri +import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.model.Account +import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName +import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType +import com.vitorpamplona.amethyst.commons.model.mediaServers.blossomUploadOrder +import com.vitorpamplona.amethyst.commons.service.upload.BlossomClient +import com.vitorpamplona.amethyst.commons.service.upload.BlossomPaymentException +import com.vitorpamplona.amethyst.commons.service.upload.FileHeader +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaCompressorResult +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri +import com.vitorpamplona.amethyst.commons.service.uploads.UploadError +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState.UploadingFinalState +import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader +import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent +import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent +import com.vitorpamplona.quartz.nipB7Blossom.BlossomServerUrl +import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.ciphers.NostrCipher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import java.io.File +import kotlin.coroutines.cancellation.CancellationException + +/** + * Android's [MediaUploader]: compresses with LightCompressor/Compressor, strips metadata, encrypts, + * and uploads over the app's upload-role OkHttp clients. Reads files through the application + * [appContext]'s content resolver, so it needs no Activity. Every entry point moves to + * [Dispatchers.IO] itself: hashing, bitmap decoding and stripping block, and shared callers may + * launch from a Main-bound scope. + */ +class AndroidMediaUploader( + private val appContext: Context, +) : MediaUploader { + override suspend fun upload( + progress: UploadOrchestrator, + uri: MediaUri, + mimeType: String?, + alt: String?, + contentWarningReason: String?, + compressionQuality: CompressorQuality, + server: ServerName, + account: Account, + useH265: Boolean, + stripMetadata: Boolean, + onStrippingFailed: suspend () -> Boolean, + convertGifToMp4: Boolean, + forcedSigner: NostrSigner?, + ): UploadingFinalState = withContext(Dispatchers.IO) { Job(progress).upload(uri, mimeType, alt, contentWarningReason, compressionQuality, server, account, appContext, useH265, stripMetadata, onStrippingFailed, convertGifToMp4, forcedSigner) } + + override suspend fun uploadEncrypted( + progress: UploadOrchestrator, + uri: MediaUri, + mimeType: String?, + alt: String?, + contentWarningReason: String?, + compressionQuality: CompressorQuality, + encrypt: NostrCipher, + server: ServerName, + account: Account, + useH265: Boolean, + stripMetadata: Boolean, + onStrippingFailed: suspend () -> Boolean, + convertGifToMp4: Boolean, + forcedSigner: NostrSigner?, + ): UploadingFinalState = withContext(Dispatchers.IO) { Job(progress).uploadEncrypted(uri, mimeType, alt, contentWarningReason, compressionQuality, encrypt, server, account, appContext, useH265, stripMetadata, onStrippingFailed, convertGifToMp4, forcedSigner) } + + override suspend fun compressIfNeeded( + progress: UploadOrchestrator, + uri: MediaUri, + mimeType: String?, + compressionQuality: CompressorQuality, + useH265: Boolean, + convertGifToMp4: Boolean, + ): MediaCompressorResult = withContext(Dispatchers.IO) { Job(progress).compressIfNeeded(uri, mimeType, compressionQuality, appContext, useH265, convertGifToMp4) } + + /** One upload's run: the pipeline, reporting into [progress]. */ + private class Job( + val progress: UploadOrchestrator, + ) { + fun error( + error: UploadError, + vararg params: String, + ) = progress.error(error, *params) + + fun finish(result: UploadOrchestrator.OrchestratorResult) = progress.finish(result) + + fun updateState( + newProgress: Double, + newState: UploadingState, + ) = progress.updateState(newProgress, newState) + + private fun uploadNIP95( + fileUri: Uri, + contentType: String?, + originalContentType: String?, + originalHash: String?, + context: Context, + ): UploadingFinalState { + updateState(0.4, UploadingState.Uploading) + + val bytes = + context.contentResolver.openInputStream(fileUri)?.use { + it.readBytes() + } + + if (bytes != null) { + if (bytes.size > 80000) { + return error(UploadError.MEDIA_TOO_BIG_FOR_NIP95) + } + + updateState(0.8, UploadingState.Hashing) + + val result = + FileHeader.prepare( + bytes, + contentType, + null, + ) + + result.fold( + onSuccess = { + return finish(UploadOrchestrator.OrchestratorResult.NIP95Result(it, bytes, originalContentType, originalHash)) + }, + onFailure = { + return error(UploadError.COULD_NOT_CHECK_DOWNLOADED_FILE, it.message ?: it.javaClass.simpleName) + }, + ) + } else { + return error(UploadError.COULD_NOT_OPEN_COMPRESSED_FILE) + } + } + + private suspend fun uploadNIP96( + fileUri: Uri, + contentType: String?, + size: Long?, + alt: String?, + contentWarningReason: String?, + serverBaseUrl: String, + contentTypeForResult: String?, + originalHash: String?, + account: Account, + forcedSigner: NostrSigner?, + context: Context, + ): UploadingFinalState { + updateState(0.2, UploadingState.Uploading) + return try { + val result = + Nip96Uploader().upload( + uri = fileUri, + contentType = contentType, + size = size, + alt = alt, + sensitiveContent = contentWarningReason, + serverBaseUrl = serverBaseUrl, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, + onProgress = { percent: Float -> + updateState(0.2 + (0.2 * percent), UploadingState.Uploading) + }, + httpAuth = + if (forcedSigner != null) { + { url, method, body -> forcedSigner.sign(HTTPAuthorizationEvent.build(url, method, body)) } + } else { + account::createHTTPAuthorization + }, + context = context, + ) + + verifyHeader( + uploadResult = result, + localContentType = contentType, + originalContentType = contentTypeForResult, + originalHash = originalHash, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, + ) + } catch (_: SignerExceptions.ReadOnlyException) { + error(UploadError.LOGIN_WITH_PRIVATE_KEY) + } catch (e: Exception) { + if (e is CancellationException) throw e + error(UploadError.FAILED_TO_UPLOAD_MEDIA, e.message ?: e.javaClass.simpleName) + } + } + + /** + * Tries [selected], then the rest of the servers the picker offered (see + * [blossomUploadOrder]) until one stores the blob. Every server gets the same blob, so they + * share one signed upload token: the signer is asked once, and a signer that refused, timed + * out or is read-only stops the fallback instead of prompting again per server. Attempts + * don't publish their errors; when every server fails, the error shown is the selected + * server's, the one the user chose and will recognize. + */ + private suspend fun uploadBlossomWithFallback( + selected: ServerName, + account: Account, + uploadTo: suspend (serverBaseUrl: String, auth: SharedUploadAuth) -> UploadingFinalState, + ): UploadingFinalState { + val order = blossomUploadOrder(selected, account.blossomServers.hostNameFlow.value) + val auth = SharedUploadAuth() + var firstError: UploadingState.Error? = null + for (server in order) { + when (val result = uploadTo(server.baseUrl, auth)) { + is UploadingState.Finished -> return result + is UploadingState.Error -> { + if (firstError == null) firstError = result + if (auth.signerFailed) return result.also { updateState(0.0, it) } + Log.w("UploadOrchestrator", "Upload to ${server.baseUrl} failed, trying the next server") + } + } + } + return firstError!!.also { updateState(0.0, it) } + } + + /** + * The upload token for one blob, signed at most once and reused by every server tried. + * A signer that returned no token is asked again on the next server; only a token or a + * signer failure is kept. + */ + private class SharedUploadAuth { + private var outcome: Result? = null + + var signerFailed = false + private set + + suspend fun get(sign: suspend () -> BlossomAuthorizationEvent?): BlossomAuthorizationEvent? { + outcome?.let { return it.getOrThrow() } + return try { + sign()?.also { outcome = Result.success(it) } + } catch (e: SignerExceptions) { + signerFailed = true + outcome = Result.failure(e) + throw e + } + } + } + + private suspend fun uploadBlossom( + fileUri: Uri, + contentType: String?, + size: Long?, + alt: String?, + contentWarningReason: String?, + serverBaseUrl: String, + contentTypeForResult: String?, + originalHash: String?, + account: Account, + forcedSigner: NostrSigner?, + context: Context, + sharedAuth: SharedUploadAuth, + ): UploadingFinalState { + updateState(0.2, UploadingState.Uploading) + // BUD-05: route through /media (optimize) when the user opted in. The forced-signer + // path (e.g. NIP-46 draft signing) always uses the bit-exact /upload. + val useMedia = forcedSigner == null && account.settings.optimizeMediaOnUpload.value + return try { + val result = + BlossomUploader() + .upload( + uri = fileUri, + contentType = contentType, + size = size, + alt = alt, + sensitiveContent = contentWarningReason, + serverBaseUrl = serverBaseUrl, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, + // Use a t=media token when optimizing via /media, otherwise a plain + // t=upload token. Tokens are intentionally NOT server-scoped on the + // upload path: some servers reject an upload whose auth carries a + // `server` tag, and upload-token replay is not the threat scoping + // guards against (delete tokens are — those stay scoped). + httpAuth = { hash, size, alt -> + sharedAuth.get { + when { + forcedSigner != null -> BlossomAuthorizationEvent.createUploadAuth(hash, size, alt, forcedSigner) + useMedia -> account.createBlossomMediaAuth(hash, size, alt) + else -> account.createBlossomUploadAuth(hash, size, alt) + } + } + }, + context = context, + useMediaEndpoint = useMedia, + ) + + val finalState = + verifyHeader( + uploadResult = result, + localContentType = contentType, + okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, + originalHash = originalHash, + originalContentType = contentTypeForResult, + ) + + // BUD-04: replicate the blob to the user's other Blossom servers for redundancy. + // Fire-and-forget on the account scope AFTER the upload is finished: mirroring is + // pure background redundancy, so it must never delay, alter, or fail the upload the + // user already completed, and must not touch the on-screen progress state. + if (finalState is UploadingState.Finished && forcedSigner == null && account.settings.mirrorUploadsToAllServers.value) { + account.scope.launch(Dispatchers.IO) { + try { + mirrorToOtherServers(result, serverBaseUrl, account) + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.w("UploadOrchestrator", "Background mirror failed", e) + } + } + } + + finalState + } catch (_: SignerExceptions.ReadOnlyException) { + UploadingState.Error(UploadError.LOGIN_WITH_PRIVATE_KEY, emptyArray()) + } catch (e: BlossomPaymentException) { + // BUD-07: the server wants payment before it will store the blob. + UploadingState.Error(UploadError.BLOSSOM_PAYMENT_REQUIRED, arrayOf(e.payment.reason ?: serverBaseUrl)) + } catch (e: Exception) { + if (e is CancellationException) throw e + UploadingState.Error(UploadError.FAILED_TO_UPLOAD_MEDIA, arrayOf(e.message?.ifBlank { null } ?: e.javaClass.simpleName)) + } + } + + /** + * BUD-04 mirror fan-out: asks every *other* Blossom server in the account's + * kind-10063 list to pull the freshly-uploaded blob from [result]'s URL. Runs + * in the background after the upload is finished (see caller), so it never + * delays the user's post or touches the upload progress UI; per-server failures + * are swallowed. Requires the blob's sha256 so server B can verify the download. + */ + private suspend fun mirrorToOtherServers( + result: MediaUploadResult, + primaryServerBaseUrl: String, + account: Account, + ) { + val sourceUrl = result.url ?: return + val hash = result.sha256 ?: sourceUrl.substringAfterLast('/').substringBefore('.') + if (hash.length != 64) return + + // Only the user's *explicitly configured* kind-10063 servers (flow), NOT the + // DEFAULT_MEDIA_SERVERS fallback that hostNameFlow injects — we must never fan + // uploads out to public defaults the user never opted into. + val primaryDomain = BlossomServerUrl.domain(primaryServerBaseUrl) + val targets = + account.blossomServers.flow.value + .filter { BlossomServerUrl.domain(it) != primaryDomain } + .distinct() + + if (targets.isEmpty()) return + + val contentType = result.type ?: "application/octet-stream" + targets.forEach { target -> + try { + val auth = account.createBlossomUploadAuth(hash, result.size ?: 0L, "Mirror $hash").toAuthorizationHeader() + BlossomClient(Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForUploads(target)) + .mirrorOrUpload(sourceUrl, hash, contentType, target, auth) + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.w("UploadOrchestrator", "Failed to mirror $hash to $target", e) + } + } + } + + private suspend fun verifyHeader( + uploadResult: MediaUploadResult, + localContentType: String?, + originalContentType: String?, + originalHash: String?, + okHttpClient: (String) -> OkHttpClient, + ): UploadingFinalState { + if (uploadResult.url.isNullOrBlank()) { + return error(UploadError.SERVER_DID_NOT_PROVIDE_URL) + } + + updateState(0.6, UploadingState.Downloading) + + // Use streaming verification for memory efficiency with large files + val verification = + ImageDownloader().waitAndVerifyStream(uploadResult.url, okHttpClient) + ?: return error(UploadError.COULD_NOT_DOWNLOAD_FROM_SERVER) + + updateState(0.8, UploadingState.Hashing) + + // Create FileHeader with hash from streaming verification + // Note: We skip blurhash/dimensions since we already have them from upload + val fileHeader = + FileHeader( + mimeType = uploadResult.type ?: localContentType ?: verification.contentType, + hash = verification.hash, + size = verification.size.toInt(), + dim = uploadResult.dimension, + blurHash = uploadResult.blurHash, + ) + + return finish( + UploadOrchestrator.OrchestratorResult.ServerResult( + fileHeader, + uploadResult.url, + uploadResult.magnet, + uploadResult.sha256, + originalContentType, + originalHash, + ), + ) + } + + suspend fun compressIfNeeded( + uri: Uri, + mimeType: String?, + compressionQuality: CompressorQuality, + context: Context, + useH265: Boolean = false, + convertGifToMp4: Boolean = false, + ) = if (compressionQuality != CompressorQuality.UNCOMPRESSED || convertGifToMp4) { + updateState(0.02, UploadingState.Compressing) + MediaCompressor().compress(uri, mimeType, compressionQuality, context.applicationContext, useH265, convertGifToMp4) + } else { + MediaCompressorResult(uri, mimeType, null) + } + + private suspend fun stripAfterCompression( + originalUri: Uri, + compressed: MediaCompressorResult, + mimeType: String?, + compressionQuality: CompressorQuality, + stripMetadata: Boolean, + onStrippingFailed: suspend () -> Boolean, + context: Context, + ): Uri? { + if (!stripMetadata) return compressed.uri + + val effectiveMimeType = compressed.contentType ?: mimeType + val isVideo = effectiveMimeType?.startsWith("video/", ignoreCase = true) == true + val compressionRequested = compressionQuality != CompressorQuality.UNCOMPRESSED + val compressionApplied = compressionRequested && compressed.uri != originalUri + + val strippingResult = + if (isVideo && compressionApplied) { + // Compression was requested and actually applied to a video; + // assume it stripped metadata successfully. + StrippingResult(compressed.uri, true) + } else { + // AvifMetadataNotVerifiableException is allowed to propagate so the caller + // can emit a specific error rather than the generic "Upload cancelled". + MetadataStripper.strip(compressed.uri, effectiveMimeType, context.applicationContext) + } + + if (!strippingResult.stripped && !onStrippingFailed()) return null + + return strippingResult.uri + } + + /** + * Deletes a temporary file created during the upload pipeline if its URI + * differs from the original (meaning it's an intermediate temp file, not the user's content). + */ + internal fun deleteTempUri( + tempUri: Uri, + originalUri: Uri, + ) { + if (tempUri == originalUri) return + try { + val path = tempUri.path ?: return + val file = File(path) + if (file.delete()) { + Log.d("UploadOrchestrator") { "Deleted temp file: $path" } + } + } catch (e: Exception) { + Log.w("UploadOrchestrator", "Failed to delete temp file: ${tempUri.path}", e) + } + } + + suspend fun upload( + uri: Uri, + mimeType: String?, + alt: String?, + contentWarningReason: String?, + compressionQuality: CompressorQuality, + server: ServerName, + account: Account, + context: Context, + useH265: Boolean = false, + stripMetadata: Boolean = true, + onStrippingFailed: suspend () -> Boolean = { true }, + convertGifToMp4: Boolean = false, + forcedSigner: NostrSigner? = null, + ): UploadingFinalState { + val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context, useH265, convertGifToMp4) + + val finalUri = + try { + stripAfterCompression(uri, compressed, mimeType, compressionQuality, stripMetadata, onStrippingFailed, context) + } catch (e: AvifMetadataNotVerifiableException) { + return error(UploadError.AVIF_METADATA_STRIP_FAILED, e.message ?: e.javaClass.simpleName).also { + deleteTempUri(compressed.uri, uri) + } + } ?: return error(UploadError.UPLOAD_CANCELLED).also { + deleteTempUri(compressed.uri, uri) + } + + if (compressed.uri != finalUri) deleteTempUri(compressed.uri, uri) + + try { + return when (server.type) { + ServerType.NIP95 -> uploadNIP95(finalUri, compressed.contentType, null, null, context) + ServerType.NIP96 -> uploadNIP96(finalUri, compressed.contentType, compressed.size, alt, contentWarningReason, server.baseUrl, null, null, account, forcedSigner, context) + ServerType.Blossom -> + uploadBlossomWithFallback(server, account) { baseUrl, auth -> + uploadBlossom(finalUri, compressed.contentType, compressed.size, alt, contentWarningReason, baseUrl, null, null, account, forcedSigner, context, auth) + } + } + } finally { + deleteTempUri(finalUri, uri) + } + } + + suspend fun uploadEncrypted( + uri: Uri, + mimeType: String?, + alt: String?, + contentWarningReason: String?, + compressionQuality: CompressorQuality, + encrypt: NostrCipher, + server: ServerName, + account: Account, + context: Context, + useH265: Boolean = false, + stripMetadata: Boolean = true, + onStrippingFailed: suspend () -> Boolean = { true }, + convertGifToMp4: Boolean = false, + forcedSigner: NostrSigner? = null, + ): UploadingFinalState { + val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context, useH265, convertGifToMp4) + + val finalUri = + try { + stripAfterCompression(uri, compressed, mimeType, compressionQuality, stripMetadata, onStrippingFailed, context) + } catch (e: AvifMetadataNotVerifiableException) { + return error(UploadError.AVIF_METADATA_STRIP_FAILED, e.message ?: e.javaClass.simpleName).also { + deleteTempUri(compressed.uri, uri) + } + } ?: return error(UploadError.UPLOAD_CANCELLED).also { + deleteTempUri(compressed.uri, uri) + } + + if (compressed.uri != finalUri) deleteTempUri(compressed.uri, uri) + + val encrypted = EncryptFiles().encryptFile(context, finalUri, encrypt) + deleteTempUri(finalUri, uri) + + try { + return when (server.type) { + ServerType.NIP95 -> uploadNIP95(encrypted.uri, encrypted.contentType, compressed.contentType, encrypted.originalHash, context) + ServerType.NIP96 -> uploadNIP96(encrypted.uri, encrypted.contentType, encrypted.size, alt, contentWarningReason, server.baseUrl, compressed.contentType, encrypted.originalHash, account, forcedSigner, context) + // The same encrypted file goes to every server tried, so its key, nonce and + // hash stay valid whichever one ends up holding it. + ServerType.Blossom -> + uploadBlossomWithFallback(server, account) { baseUrl, auth -> + uploadBlossom(encrypted.uri, encrypted.contentType, encrypted.size, alt, contentWarningReason, baseUrl, compressed.contentType, encrypted.originalHash, account, forcedSigner, context, auth) + } + } + } finally { + deleteTempUri(encrypted.uri, uri) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaCompressor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaCompressor.kt index 1c8bb3e12a..d2fa109863 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaCompressor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaCompressor.kt @@ -26,7 +26,9 @@ import android.net.Uri import androidx.core.net.toUri import androidx.media3.common.MimeTypes import com.davotoula.lightcompressor.video.GifToMp4Converter -import com.vitorpamplona.amethyst.service.checkNotInMainThread +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaCompressorResult +import com.vitorpamplona.amethyst.commons.service.uploads.isAvif import com.vitorpamplona.amethyst.ui.components.util.MediaCompressorFileUtils import com.vitorpamplona.quartz.utils.Log import id.zelory.compressor.Compressor @@ -34,12 +36,6 @@ import id.zelory.compressor.constraint.default import kotlinx.coroutines.CancellationException import java.io.File -class MediaCompressorResult( - val uri: Uri, - val contentType: String?, - val size: Long?, -) - class MediaCompressor { // ALL ERRORS ARE IGNORED. The original file is returned. suspend fun compress( @@ -50,8 +46,6 @@ class MediaCompressor { useH265: Boolean = false, convertGifToMp4: Boolean = false, ): MediaCompressorResult { - checkNotInMainThread() - // Convert GIF to MP4 if requested. The GIF converter already produces a well-compressed // H.264 MP4 so no additional video compression step is needed. if (convertGifToMp4 && contentType?.contains("gif", ignoreCase = true) == true) { @@ -127,24 +121,4 @@ class MediaCompressor { MediaCompressorResult(uri, contentType, null) } } - - companion object { - fun intToCompressorQuality(mediaQualityFloat: Int): CompressorQuality = - when (mediaQualityFloat) { - 0 -> CompressorQuality.LOW - 1 -> CompressorQuality.MEDIUM - 2 -> CompressorQuality.HIGH - 3 -> CompressorQuality.UNCOMPRESSED - else -> CompressorQuality.MEDIUM - } - } -} - -enum class CompressorQuality { - VERY_LOW, - LOW, - MEDIUM, - HIGH, - VERY_HIGH, - UNCOMPRESSED, } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MetadataStripper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MetadataStripper.kt index 1d29223977..85c0bf54eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MetadataStripper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MetadataStripper.kt @@ -29,6 +29,7 @@ import android.media.MediaMuxer import android.net.Uri import androidx.core.net.toUri import androidx.exifinterface.media.ExifInterface +import com.vitorpamplona.amethyst.commons.service.uploads.isAvif import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException import java.io.File diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/PreviewMetadataCalculator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/PreviewMetadataCalculator.kt index df2756435b..840a0c13da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/PreviewMetadataCalculator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/PreviewMetadataCalculator.kt @@ -31,6 +31,7 @@ import androidx.annotation.RequiresApi import com.vitorpamplona.amethyst.commons.blurhash.toBlurhash import com.vitorpamplona.amethyst.commons.service.image.BlurhashWrapper import com.vitorpamplona.amethyst.commons.service.image.ThumbhashWrapper +import com.vitorpamplona.amethyst.commons.service.uploads.isAvif import com.vitorpamplona.amethyst.commons.thumbhash.toThumbhash import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import com.vitorpamplona.quartz.utils.Log diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt deleted file mode 100644 index af41342da5..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt +++ /dev/null @@ -1,602 +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.service.uploads - -import android.content.Context -import android.net.Uri -import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.commons.model.Account -import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName -import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType -import com.vitorpamplona.amethyst.commons.model.mediaServers.blossomUploadOrder -import com.vitorpamplona.amethyst.commons.resources.Res -import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed -import com.vitorpamplona.amethyst.commons.resources.blossom_payment_required -import com.vitorpamplona.amethyst.commons.resources.could_not_check_downloaded_file -import com.vitorpamplona.amethyst.commons.resources.could_not_download_from_the_server -import com.vitorpamplona.amethyst.commons.resources.could_not_open_the_compressed_file -import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media -import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_upload -import com.vitorpamplona.amethyst.commons.resources.media_too_big_for_nip95 -import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading -import com.vitorpamplona.amethyst.commons.resources.upload_cancelled -import com.vitorpamplona.amethyst.commons.service.upload.BlossomClient -import com.vitorpamplona.amethyst.commons.service.upload.BlossomPaymentException -import com.vitorpamplona.amethyst.commons.service.upload.FileHeader -import com.vitorpamplona.amethyst.service.uploads.UploadingState.UploadingFinalState -import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader -import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions -import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent -import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent -import com.vitorpamplona.quartz.nipB7Blossom.BlossomServerUrl -import com.vitorpamplona.quartz.utils.Log -import com.vitorpamplona.quartz.utils.ciphers.NostrCipher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import okhttp3.OkHttpClient -import org.jetbrains.compose.resources.StringResource -import java.io.File -import kotlin.coroutines.cancellation.CancellationException - -sealed class UploadingState { - data object Ready : UploadingState() - - data object Compressing : UploadingState() - - data object Uploading : UploadingState() - - data object ServerProcessing : UploadingState() - - data object Downloading : UploadingState() - - data object Hashing : UploadingState() - - sealed class UploadingFinalState : UploadingState() - - class Finished( - val result: UploadOrchestrator.OrchestratorResult, - ) : UploadingFinalState() - - class Error( - val errorResource: StringResource, - val params: Array, - ) : UploadingFinalState() -} - -class UploadOrchestrator { - val progress = MutableStateFlow(0.0) - val progressState = MutableStateFlow(UploadingState.Ready) - - val isUploading = - progressState.map { - progressState.value !is UploadingState.Ready && progressState.value !is UploadingState.Error && progressState.value !is UploadingState.Finished - } - - fun error( - resId: StringResource, - vararg params: String, - ) = UploadingState.Error(resId, params).also { updateState(0.0, it) } - - fun finish(result: OrchestratorResult) = - UploadingState - .Finished(result) - .also { updateState(1.0, it) } - - fun updateState( - newProgress: Double, - newState: UploadingState, - ) { - progress.value = newProgress - progressState.value = newState - } - - private fun uploadNIP95( - fileUri: Uri, - contentType: String?, - originalContentType: String?, - originalHash: String?, - context: Context, - ): UploadingFinalState { - updateState(0.4, UploadingState.Uploading) - - val bytes = - context.contentResolver.openInputStream(fileUri)?.use { - it.readBytes() - } - - if (bytes != null) { - if (bytes.size > 80000) { - return error(Res.string.media_too_big_for_nip95) - } - - updateState(0.8, UploadingState.Hashing) - - val result = - FileHeader.prepare( - bytes, - contentType, - null, - ) - - result.fold( - onSuccess = { - return finish(OrchestratorResult.NIP95Result(it, bytes, originalContentType, originalHash)) - }, - onFailure = { - return error(Res.string.could_not_check_downloaded_file, it.message ?: it.javaClass.simpleName) - }, - ) - } else { - return error(Res.string.could_not_open_the_compressed_file) - } - } - - private suspend fun uploadNIP96( - fileUri: Uri, - contentType: String?, - size: Long?, - alt: String?, - contentWarningReason: String?, - serverBaseUrl: String, - contentTypeForResult: String?, - originalHash: String?, - account: Account, - forcedSigner: NostrSigner?, - context: Context, - ): UploadingFinalState { - updateState(0.2, UploadingState.Uploading) - return try { - val result = - Nip96Uploader().upload( - uri = fileUri, - contentType = contentType, - size = size, - alt = alt, - sensitiveContent = contentWarningReason, - serverBaseUrl = serverBaseUrl, - okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, - onProgress = { percent: Float -> - updateState(0.2 + (0.2 * percent), UploadingState.Uploading) - }, - httpAuth = - if (forcedSigner != null) { - { url, method, body -> forcedSigner.sign(HTTPAuthorizationEvent.build(url, method, body)) } - } else { - account::createHTTPAuthorization - }, - context = context, - ) - - verifyHeader( - uploadResult = result, - localContentType = contentType, - originalContentType = contentTypeForResult, - originalHash = originalHash, - okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, - ) - } catch (_: SignerExceptions.ReadOnlyException) { - error(Res.string.login_with_a_private_key_to_be_able_to_upload) - } catch (e: Exception) { - if (e is CancellationException) throw e - error(Res.string.failed_to_upload_media, e.message ?: e.javaClass.simpleName) - } - } - - /** - * Tries [selected], then the rest of the servers the picker offered (see - * [blossomUploadOrder]) until one stores the blob. Every server gets the same blob, so they - * share one signed upload token: the signer is asked once, and a signer that refused, timed - * out or is read-only stops the fallback instead of prompting again per server. Attempts - * don't publish their errors; when every server fails, the error shown is the selected - * server's, the one the user chose and will recognize. - */ - private suspend fun uploadBlossomWithFallback( - selected: ServerName, - account: Account, - uploadTo: suspend (serverBaseUrl: String, auth: SharedUploadAuth) -> UploadingFinalState, - ): UploadingFinalState { - val order = blossomUploadOrder(selected, account.blossomServers.hostNameFlow.value) - val auth = SharedUploadAuth() - var firstError: UploadingState.Error? = null - for (server in order) { - when (val result = uploadTo(server.baseUrl, auth)) { - is UploadingState.Finished -> return result - is UploadingState.Error -> { - if (firstError == null) firstError = result - if (auth.signerFailed) return result.also { updateState(0.0, it) } - Log.w("UploadOrchestrator", "Upload to ${server.baseUrl} failed, trying the next server") - } - } - } - return firstError!!.also { updateState(0.0, it) } - } - - /** - * The upload token for one blob, signed at most once and reused by every server tried. - * A signer that returned no token is asked again on the next server; only a token or a - * signer failure is kept. - */ - private class SharedUploadAuth { - private var outcome: Result? = null - - var signerFailed = false - private set - - suspend fun get(sign: suspend () -> BlossomAuthorizationEvent?): BlossomAuthorizationEvent? { - outcome?.let { return it.getOrThrow() } - return try { - sign()?.also { outcome = Result.success(it) } - } catch (e: SignerExceptions) { - signerFailed = true - outcome = Result.failure(e) - throw e - } - } - } - - private suspend fun uploadBlossom( - fileUri: Uri, - contentType: String?, - size: Long?, - alt: String?, - contentWarningReason: String?, - serverBaseUrl: String, - contentTypeForResult: String?, - originalHash: String?, - account: Account, - forcedSigner: NostrSigner?, - context: Context, - sharedAuth: SharedUploadAuth, - ): UploadingFinalState { - updateState(0.2, UploadingState.Uploading) - // BUD-05: route through /media (optimize) when the user opted in. The forced-signer - // path (e.g. NIP-46 draft signing) always uses the bit-exact /upload. - val useMedia = forcedSigner == null && account.settings.optimizeMediaOnUpload.value - return try { - val result = - BlossomUploader() - .upload( - uri = fileUri, - contentType = contentType, - size = size, - alt = alt, - sensitiveContent = contentWarningReason, - serverBaseUrl = serverBaseUrl, - okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, - // Use a t=media token when optimizing via /media, otherwise a plain - // t=upload token. Tokens are intentionally NOT server-scoped on the - // upload path: some servers reject an upload whose auth carries a - // `server` tag, and upload-token replay is not the threat scoping - // guards against (delete tokens are — those stay scoped). - httpAuth = { hash, size, alt -> - sharedAuth.get { - when { - forcedSigner != null -> BlossomAuthorizationEvent.createUploadAuth(hash, size, alt, forcedSigner) - useMedia -> account.createBlossomMediaAuth(hash, size, alt) - else -> account.createBlossomUploadAuth(hash, size, alt) - } - } - }, - context = context, - useMediaEndpoint = useMedia, - ) - - val finalState = - verifyHeader( - uploadResult = result, - localContentType = contentType, - okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, - originalHash = originalHash, - originalContentType = contentTypeForResult, - ) - - // BUD-04: replicate the blob to the user's other Blossom servers for redundancy. - // Fire-and-forget on the account scope AFTER the upload is finished: mirroring is - // pure background redundancy, so it must never delay, alter, or fail the upload the - // user already completed, and must not touch the on-screen progress state. - if (finalState is UploadingState.Finished && forcedSigner == null && account.settings.mirrorUploadsToAllServers.value) { - account.scope.launch(Dispatchers.IO) { - try { - mirrorToOtherServers(result, serverBaseUrl, account) - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.w("UploadOrchestrator", "Background mirror failed", e) - } - } - } - - finalState - } catch (_: SignerExceptions.ReadOnlyException) { - UploadingState.Error(Res.string.login_with_a_private_key_to_be_able_to_upload, emptyArray()) - } catch (e: BlossomPaymentException) { - // BUD-07: the server wants payment before it will store the blob. - UploadingState.Error(Res.string.blossom_payment_required, arrayOf(e.payment.reason ?: serverBaseUrl)) - } catch (e: Exception) { - if (e is CancellationException) throw e - UploadingState.Error(Res.string.failed_to_upload_media, arrayOf(e.message?.ifBlank { null } ?: e.javaClass.simpleName)) - } - } - - /** - * BUD-04 mirror fan-out: asks every *other* Blossom server in the account's - * kind-10063 list to pull the freshly-uploaded blob from [result]'s URL. Runs - * in the background after the upload is finished (see caller), so it never - * delays the user's post or touches the upload progress UI; per-server failures - * are swallowed. Requires the blob's sha256 so server B can verify the download. - */ - private suspend fun mirrorToOtherServers( - result: MediaUploadResult, - primaryServerBaseUrl: String, - account: Account, - ) { - val sourceUrl = result.url ?: return - val hash = result.sha256 ?: sourceUrl.substringAfterLast('/').substringBefore('.') - if (hash.length != 64) return - - // Only the user's *explicitly configured* kind-10063 servers (flow), NOT the - // DEFAULT_MEDIA_SERVERS fallback that hostNameFlow injects — we must never fan - // uploads out to public defaults the user never opted into. - val primaryDomain = BlossomServerUrl.domain(primaryServerBaseUrl) - val targets = - account.blossomServers.flow.value - .filter { BlossomServerUrl.domain(it) != primaryDomain } - .distinct() - - if (targets.isEmpty()) return - - val contentType = result.type ?: "application/octet-stream" - targets.forEach { target -> - try { - val auth = account.createBlossomUploadAuth(hash, result.size ?: 0L, "Mirror $hash").toAuthorizationHeader() - BlossomClient(Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForUploads(target)) - .mirrorOrUpload(sourceUrl, hash, contentType, target, auth) - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.w("UploadOrchestrator", "Failed to mirror $hash to $target", e) - } - } - } - - private suspend fun verifyHeader( - uploadResult: MediaUploadResult, - localContentType: String?, - originalContentType: String?, - originalHash: String?, - okHttpClient: (String) -> OkHttpClient, - ): UploadingFinalState { - if (uploadResult.url.isNullOrBlank()) { - return error(Res.string.server_did_not_provide_a_url_after_uploading) - } - - updateState(0.6, UploadingState.Downloading) - - // Use streaming verification for memory efficiency with large files - val verification = - ImageDownloader().waitAndVerifyStream(uploadResult.url, okHttpClient) - ?: return error(Res.string.could_not_download_from_the_server) - - updateState(0.8, UploadingState.Hashing) - - // Create FileHeader with hash from streaming verification - // Note: We skip blurhash/dimensions since we already have them from upload - val fileHeader = - FileHeader( - mimeType = uploadResult.type ?: localContentType ?: verification.contentType, - hash = verification.hash, - size = verification.size.toInt(), - dim = uploadResult.dimension, - blurHash = uploadResult.blurHash, - ) - - return finish( - OrchestratorResult.ServerResult( - fileHeader, - uploadResult.url, - uploadResult.magnet, - uploadResult.sha256, - originalContentType, - originalHash, - ), - ) - } - - sealed class OrchestratorResult { - class NIP95Result( - val fileHeader: FileHeader, - val bytes: ByteArray, - val mimeTypeBeforeEncryption: String?, - val hashBeforeEncryption: String?, - ) : OrchestratorResult() - - class ServerResult( - val fileHeader: FileHeader, - val url: String, - val magnet: String?, - val uploadedHash: String?, - val mimeTypeBeforeEncryption: String?, - val hashBeforeEncryption: String?, - ) : OrchestratorResult() - } - - suspend fun compressIfNeeded( - uri: Uri, - mimeType: String?, - compressionQuality: CompressorQuality, - context: Context, - useH265: Boolean = false, - convertGifToMp4: Boolean = false, - ) = if (compressionQuality != CompressorQuality.UNCOMPRESSED || convertGifToMp4) { - updateState(0.02, UploadingState.Compressing) - MediaCompressor().compress(uri, mimeType, compressionQuality, context.applicationContext, useH265, convertGifToMp4) - } else { - MediaCompressorResult(uri, mimeType, null) - } - - private suspend fun stripAfterCompression( - originalUri: Uri, - compressed: MediaCompressorResult, - mimeType: String?, - compressionQuality: CompressorQuality, - stripMetadata: Boolean, - onStrippingFailed: suspend () -> Boolean, - context: Context, - ): Uri? { - if (!stripMetadata) return compressed.uri - - val effectiveMimeType = compressed.contentType ?: mimeType - val isVideo = effectiveMimeType?.startsWith("video/", ignoreCase = true) == true - val compressionRequested = compressionQuality != CompressorQuality.UNCOMPRESSED - val compressionApplied = compressionRequested && compressed.uri != originalUri - - val strippingResult = - if (isVideo && compressionApplied) { - // Compression was requested and actually applied to a video; - // assume it stripped metadata successfully. - StrippingResult(compressed.uri, true) - } else { - // AvifMetadataNotVerifiableException is allowed to propagate so the caller - // can emit a specific error rather than the generic "Upload cancelled". - MetadataStripper.strip(compressed.uri, effectiveMimeType, context.applicationContext) - } - - if (!strippingResult.stripped && !onStrippingFailed()) return null - - return strippingResult.uri - } - - /** - * Deletes a temporary file created during the upload pipeline if its URI - * differs from the original (meaning it's an intermediate temp file, not the user's content). - */ - internal fun deleteTempUri( - tempUri: Uri, - originalUri: Uri, - ) { - if (tempUri == originalUri) return - try { - val path = tempUri.path ?: return - val file = File(path) - if (file.delete()) { - Log.d("UploadOrchestrator") { "Deleted temp file: $path" } - } - } catch (e: Exception) { - Log.w("UploadOrchestrator", "Failed to delete temp file: ${tempUri.path}", e) - } - } - - suspend fun upload( - uri: Uri, - mimeType: String?, - alt: String?, - contentWarningReason: String?, - compressionQuality: CompressorQuality, - server: ServerName, - account: Account, - context: Context, - useH265: Boolean = false, - stripMetadata: Boolean = true, - onStrippingFailed: suspend () -> Boolean = { true }, - convertGifToMp4: Boolean = false, - forcedSigner: NostrSigner? = null, - ): UploadingFinalState { - val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context, useH265, convertGifToMp4) - - val finalUri = - try { - stripAfterCompression(uri, compressed, mimeType, compressionQuality, stripMetadata, onStrippingFailed, context) - } catch (e: AvifMetadataNotVerifiableException) { - return error(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName).also { - deleteTempUri(compressed.uri, uri) - } - } ?: return error(Res.string.upload_cancelled).also { - deleteTempUri(compressed.uri, uri) - } - - if (compressed.uri != finalUri) deleteTempUri(compressed.uri, uri) - - try { - return when (server.type) { - ServerType.NIP95 -> uploadNIP95(finalUri, compressed.contentType, null, null, context) - ServerType.NIP96 -> uploadNIP96(finalUri, compressed.contentType, compressed.size, alt, contentWarningReason, server.baseUrl, null, null, account, forcedSigner, context) - ServerType.Blossom -> - uploadBlossomWithFallback(server, account) { baseUrl, auth -> - uploadBlossom(finalUri, compressed.contentType, compressed.size, alt, contentWarningReason, baseUrl, null, null, account, forcedSigner, context, auth) - } - } - } finally { - deleteTempUri(finalUri, uri) - } - } - - suspend fun uploadEncrypted( - uri: Uri, - mimeType: String?, - alt: String?, - contentWarningReason: String?, - compressionQuality: CompressorQuality, - encrypt: NostrCipher, - server: ServerName, - account: Account, - context: Context, - useH265: Boolean = false, - stripMetadata: Boolean = true, - onStrippingFailed: suspend () -> Boolean = { true }, - convertGifToMp4: Boolean = false, - forcedSigner: NostrSigner? = null, - ): UploadingFinalState { - val compressed = compressIfNeeded(uri, mimeType, compressionQuality, context, useH265, convertGifToMp4) - - val finalUri = - try { - stripAfterCompression(uri, compressed, mimeType, compressionQuality, stripMetadata, onStrippingFailed, context) - } catch (e: AvifMetadataNotVerifiableException) { - return error(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName).also { - deleteTempUri(compressed.uri, uri) - } - } ?: return error(Res.string.upload_cancelled).also { - deleteTempUri(compressed.uri, uri) - } - - if (compressed.uri != finalUri) deleteTempUri(compressed.uri, uri) - - val encrypted = EncryptFiles().encryptFile(context, finalUri, encrypt) - deleteTempUri(finalUri, uri) - - try { - return when (server.type) { - ServerType.NIP95 -> uploadNIP95(encrypted.uri, encrypted.contentType, compressed.contentType, encrypted.originalHash, context) - ServerType.NIP96 -> uploadNIP96(encrypted.uri, encrypted.contentType, encrypted.size, alt, contentWarningReason, server.baseUrl, compressed.contentType, encrypted.originalHash, account, forcedSigner, context) - // The same encrypted file goes to every server tried, so its key, nonce and - // hash stay valid whichever one ends up holding it. - ServerType.Blossom -> - uploadBlossomWithFallback(server, account) { baseUrl, auth -> - uploadBlossom(encrypted.uri, encrypted.contentType, encrypted.size, alt, contentWarningReason, baseUrl, compressed.contentType, encrypted.originalHash, account, forcedSigner, context, auth) - } - } - } finally { - deleteTempUri(encrypted.uri, uri) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt index 8b9a7ef3c0..615e23e5a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/VideoCompressionHelper.kt @@ -33,6 +33,8 @@ import com.davotoula.lightcompressor.VideoCompressor import com.davotoula.lightcompressor.config.AppSpecificStorageConfiguration import com.davotoula.lightcompressor.config.Configuration import com.davotoula.lightcompressor.config.VideoResizer +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaCompressorResult import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.LogLevel import kotlinx.coroutines.suspendCancellableCoroutine diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt index 3367d8b7ab..f8aaa70f92 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt @@ -30,11 +30,10 @@ import com.vitorpamplona.amethyst.commons.resources.failed_to_delete_with_messag import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_to_server_with_message import com.vitorpamplona.amethyst.commons.service.HttpStatusMessages import com.vitorpamplona.amethyst.commons.service.upload.BlossomPaymentException +import com.vitorpamplona.amethyst.commons.service.uploads.extensionFromMimeType import com.vitorpamplona.amethyst.commons.ui.loadStringRes -import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult import com.vitorpamplona.amethyst.service.uploads.PreviewMetadataCalculator -import com.vitorpamplona.amethyst.service.uploads.extensionFromMimeType import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.JsonMapper import com.vitorpamplona.quartz.nip01Core.core.toHexKey @@ -86,8 +85,6 @@ class BlossomUploader { useMediaEndpoint: Boolean = false, onProgress: ((bytesWritten: Long, totalBytes: Long) -> Unit)? = null, ): MediaUploadResult { - checkNotInMainThread() - val contentResolver = context.contentResolver val myContentType = contentType ?: contentResolver.getType(uri) val fileName = context.getFileName(uri) @@ -141,8 +138,6 @@ class BlossomUploader { useMediaEndpoint: Boolean = false, onProgress: ((bytesWritten: Long, totalBytes: Long) -> Unit)? = null, ): MediaUploadResult { - checkNotInMainThread() - val fileName = baseFileName ?: RandomInstance.randomChars(16) val extension = contentType?.let { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt index 5b5fa59daa..2f0b666713 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt @@ -31,10 +31,9 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.failed_to_delete_with_message import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_to_server_with_message import com.vitorpamplona.amethyst.commons.service.HttpStatusMessages +import com.vitorpamplona.amethyst.commons.service.uploads.AVIF_EXTENSION +import com.vitorpamplona.amethyst.commons.service.uploads.AVIF_MIME import com.vitorpamplona.amethyst.commons.ui.loadStringRes -import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.amethyst.service.uploads.AVIF_EXTENSION -import com.vitorpamplona.amethyst.service.uploads.AVIF_MIME import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult import com.vitorpamplona.amethyst.service.uploads.PreviewMetadataCalculator import com.vitorpamplona.quartz.nip01Core.core.JsonMapper @@ -105,8 +104,6 @@ class Nip96Uploader { httpAuth: suspend (String, String, ByteArray?) -> HTTPAuthorizationEvent?, context: Context, ): MediaUploadResult { - checkNotInMainThread() - val contentResolver = context.contentResolver val myContentType = contentType ?: contentResolver.getType(uri) val length = size ?: contentResolver.querySize(uri) ?: fileSize(uri) ?: 0 @@ -145,8 +142,6 @@ class Nip96Uploader { httpAuth: suspend (String, String, ByteArray?) -> HTTPAuthorizationEvent?, context: Context, ): MediaUploadResult { - checkNotInMainThread() - val fileName = RandomInstance.randomChars(16) val extension = contentType?.let { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 33902c8bde..1e0a0202c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -27,51 +27,19 @@ import androidx.activity.enableEdgeToEdge import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.commons.actions.ConcordActions -import com.vitorpamplona.amethyst.commons.model.Account -import com.vitorpamplona.amethyst.commons.model.cache.LocalCache -import com.vitorpamplona.amethyst.commons.model.navigation.Route -import com.vitorpamplona.amethyst.commons.model.navigation.routeFor -import com.vitorpamplona.amethyst.commons.richtext.RichTextParser -import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor -import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeForPointer import com.vitorpamplona.amethyst.commons.ui.note.elements.NowProvider import com.vitorpamplona.amethyst.debugState import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService import com.vitorpamplona.amethyst.service.notifications.NotificationRelayService -import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia -import com.vitorpamplona.amethyst.ui.navigation.findParameterValue -import com.vitorpamplona.amethyst.ui.navigation.findQueryParameterValue import com.vitorpamplona.amethyst.ui.screen.AccountScreen import com.vitorpamplona.amethyst.ui.theme.AmethystTheme -import com.vitorpamplona.quartz.buzz.invite.BuzzInviteLink -import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent -import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey -import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser -import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress -import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed -import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent -import com.vitorpamplona.quartz.nip19Bech32.entities.NNote -import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile -import com.vitorpamplona.quartz.nip19Bech32.entities.NPub -import com.vitorpamplona.quartz.nip29RelayGroups.GroupInviteLink -import com.vitorpamplona.quartz.nip29RelayGroups.GroupNAddrInvite -import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent -import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectURI -import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect -import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent -import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId import com.vitorpamplona.quartz.utils.Log -import com.vitorpamplona.quartz.utils.UriParser -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import java.net.URLDecoder class MainActivity : AppCompatActivity() { companion object { @@ -155,339 +123,3 @@ class MainActivity : AppCompatActivity() { super.onDestroy() } } - -private const val NOSTR_URI_PREFIX = "nostr:" - -fun isNotificationRoute(uri: String) = uri.startsWith("notifications", true) || uri.startsWith("nostr:notifications", true) - -fun isHashtagRoute(uri: String) = uri.startsWith("hashtag?id=") || uri.startsWith("nostr:hashtag?id=") - -/** - * A markdown link target that is a bare hashtag fragment, e.g. the `#NostrMultiplayerGames` in - * `[Games](#NostrMultiplayerGames)`. Returns the tag without the leading `#`, or null when the - * uri is anything else (full URLs with anchors don't start with `#`). Reuses the same character - * class that linkifies #hashtags in plain text, so validity matches how tags parse everywhere else. - */ -fun fragmentHashtagOrNull(uri: String): String? { - val match = RichTextParser.hashTagsPattern.matchEntire(uri) ?: return null - if (!match.groups[2]?.value.isNullOrEmpty()) return null - return match.groups[1]?.value -} - -fun isUrlRoute(uri: String) = uri.startsWith("url?id=") || uri.startsWith("nostr:url?id=") - -/** - * A private chatroom, addressed by its participants rather than by a message. - * Posted by DM notifications — see [NotificationRoutes.chatroomUri] for why a DM - * cannot deep-link through its own note. - */ -fun isChatroomRoute(uri: String) = uri.startsWith("chatroom?id=") || uri.startsWith("nostr:chatroom?id=") - -/** - * Note what this deliberately does *not* do: register the room on the account. - * - * `routeToMessage` would, and that is right for the in-app callers, but wrong here twice over. - * `uriToRoute` runs against whichever account is current, *before* `?account=` is read and the - * switch happens (`AppNavigation.NavigateIfIntentRequested`), so a DM notification for account B - * tapped while A is on screen would insert an empty conversation into **A**'s message list. And - * `nostr:` is an exported, browsable scheme, so any web page could post - * `nostr:chatroom?id=&account=…` and inject a room of its choosing. - * - * Nothing needs it: `ChatroomFeedFilter.chatroom()` calls `getOrCreatePrivateChatroom` when the - * screen actually opens, by which point the switch has happened and the room lands on the right - * account. - * - * The ids are still checked here — a room key is a set of pubkeys, so anything that isn't one is - * not a room, and a bad deep link should resolve to nothing rather than to an unopenable screen. - */ -fun chatroomRoute(uri: String): Route? { - val users = - uri - .findQueryParameterValue("id") - ?.split(',') - ?.map { it.trim() } - ?.takeIf { it.isNotEmpty() && it.all(::isPubKeyHex) } - ?.toSet() ?: return null - - return Route.Room(ChatroomKey(users)) -} - -/** A bare 32-byte lowercase-hex pubkey, the only thing a chatroom key is made of. */ -private fun isPubKeyHex(value: String) = value.length == 64 && value.all { it in '0'..'9' || it in 'a'..'f' } - -/** - * A NIP-17 private note or reply, addressed by the rumor's own id because its `nevent` - * names the undeliverable gift wrap instead — see [NotificationRoutes.privateNoteUri]. - */ -fun isPrivateNoteRoute(uri: String) = uri.startsWith("privatenote?id=") || uri.startsWith("nostr:privatenote?id=") - -fun privateNoteRoute(uri: String): Route? { - val id = uri.findQueryParameterValue("id") ?: return null - return Route.EventRedirect(id, isPrivate = true) -} - -fun isConnectedAppRoute(uri: String) = uri.startsWith("connectedapp?coordinate=") || uri.startsWith("nostr:connectedapp?coordinate=") - -/** - * The Connected Apps permission-detail route for an app coordinate (`pubkey:dtag` for a napplet/nsite, - * `browser:` for a web client). Fired by the sandbox host so a running full-screen surface can - * jump straight to its editable permissions; the coordinate is URL-encoded into the [coordinate] param. - */ -fun connectedAppRoute(uri: String): Route.ConnectedAppDetail? { - val coordinate = - runCatching { - val raw = java.net.URI(uri.removePrefix(NOSTR_URI_PREFIX)).findParameterValue("coordinate") ?: return null - URLDecoder.decode(raw, Charsets.UTF_8.name()) - }.getOrNull()?.takeIf { it.isNotBlank() } ?: return null - - return Route.ConnectedAppDetail(coordinate) -} - -fun urlRoute(uri: String): Route.Url? { - val url = - runCatching { - val rawUrl = java.net.URI(uri.removePrefix(NOSTR_URI_PREFIX)).findParameterValue("id") ?: return null - URLDecoder.decode(rawUrl, Charsets.UTF_8.name()) - }.getOrNull() ?: return null - - return UrlId.toScopeOrNull(url)?.let { Route.Url(it) } -} - -fun isWalletConnectRoute(uri: String) = uri.startsWith("dlnwc?value=") || uri.startsWith("amethyst+walletconnect:dlnwc?value=") || uri.startsWith("amethyst+walletconnect://dlnwc?value=") - -fun isMarmotGroupRoute(uri: String) = uri.startsWith("marmot:") - -/** - * Tapping the always-on service notification. That notification's whole subject is the relay - * connections it is holding open, so it lands on the screen that explains them rather than on - * whatever tab happened to be last open. - */ -fun isActiveSubscriptionsRoute(uri: String) = uri.startsWith("activesubs", true) || uri.startsWith("nostr:activesubs", true) - -/** - * The launcher shortcut (res/xml/shortcuts.xml) fires `amethyst:scanqr`, caught by MainActivity's - * existing scheme filter. A pseudo-uri rather than a hardcoded component, because debug and - * benchmark builds carry an applicationId suffix a hardcoded targetPackage would miss, and - * manifest placeholders are not substituted into resource XML. - * - * `nostr:scanqr` is still accepted so an older pinned shortcut keeps working, but the shortcut - * itself uses our own scheme: an implicit `nostr:` VIEW intent can be answered by any Nostr client - * installed alongside us. - */ -fun isScanQrRoute(uri: String) = - uri.equals("scanqr", true) || - uri.equals("amethyst:scanqr", true) || - uri.equals("nostr:scanqr", true) - -private val MARMOT_HEX = Regex("^[0-9a-fA-F]+$") - -fun uriToRoute( - uri: String, - account: Account, -): Route? { - if (isNotificationRoute(uri)) { - val scrollTo = uri.findQueryParameterValue("scrollTo") - return Route.Notification(scrollToEventId = scrollTo) - } - if (isActiveSubscriptionsRoute(uri)) { - return Route.ActiveSubscriptions - } - if (isScanQrRoute(uri)) { - return Route.QRDisplay(account.signer.pubKey, startScanning = true) - } - if (isHashtagRoute(uri)) { - return Route.Hashtag(uri.removePrefix(NOSTR_URI_PREFIX).removePrefix("hashtag?id=").lowercase()) - } - fragmentHashtagOrNull(uri)?.let { - return Route.Hashtag(it.lowercase()) - } - if (isUrlRoute(uri)) { - return urlRoute(uri) - } - if (isChatroomRoute(uri)) { - return chatroomRoute(uri) - } - if (isPrivateNoteRoute(uri)) { - return privateNoteRoute(uri) - } - if (isConnectedAppRoute(uri)) { - return connectedAppRoute(uri) - } - - // A scanned/opened `nostrconnect://` offer is an app asking to connect to our signer: open the - // NIP-46 signer screen and let it run the pairing (it enables the signer as part of connecting). - if (uri.startsWith(NostrConnectURI.NOSTRCONNECT_SCHEME)) { - return Route.Nip46Signer(connectUri = uri) - } - - relayGroupInviteRoute(uri)?.let { return it } - concordInviteRoute(uri)?.let { return it } - buzzInviteRoute(uri)?.let { return it } - - val parsedNip19 = Nip19Parser.uriToRoute(uri) - val nip19 = parsedNip19?.entity - if (nip19 != null) { - LocalCache.consume(nip19) - - val route = - when (nip19) { - is NPub -> { - Route.Profile(nip19.hex) - } - - is NProfile -> { - Route.Profile(nip19.hex) - } - - is NNote -> { - Route.Note(nip19.hex) - } - - is NEvent -> { - val note = LocalCache.getOrCreateNote(nip19.hex) - // Only fall back to the pointer's own kind while the body is missing: once the - // event is cached it may belong somewhere the kind alone can't name (a channel, - // a chatroom), and routeFor knows that. - if (note.event == null) { - routeForPointer(nip19.kind, nip19.hex) ?: Route.EventRedirect(nip19.hex) - } else { - routeFor(note = note, loggedIn = account) ?: Route.EventRedirect(nip19.hex) - } - } - - is NAddress -> { - relayGroupDirectRoute(nip19, parsedNip19.additionalChars) - ?: routeFor( - note = LocalCache.getOrCreateAddressableNote(nip19.address()), - loggedIn = account, - ) ?: calendarDirectRoute(nip19) ?: Route.EventRedirect(nip19.aTag()) - } - - is NEmbed -> { - val noteEvent = nip19.event - if (noteEvent is AddressableEvent) { - routeFor( - note = LocalCache.getOrCreateAddressableNote(noteEvent.address()), - loggedIn = account, - ) ?: Route.EventRedirect(noteEvent.addressTag()) - } else { - routeFor( - note = LocalCache.getOrCreateNote(nip19.event.id), - loggedIn = account, - ) ?: Route.EventRedirect(nip19.event.id) - } - } - - else -> { - null - } - } - - if (route != null) { - return route - } - } - - if (isMarmotGroupRoute(uri)) { - // marmot:?account= - val groupHex = - uri - .removePrefix("marmot:") - .substringBefore("?") - .substringBefore("&") - if (groupHex.matches(MARMOT_HEX)) { - return Route.MarmotGroupChat(groupHex) - } - } - - if (isWalletConnectRoute(uri)) { - try { - val url = UriParser(uri) - val nip47Uri = url.getQueryParameter("value")?.firstOrNull() - if (nip47Uri != null) { - Nip47WalletConnect.parse(nip47Uri) - return Route.WalletAddNwc(nip47Uri) - } - } catch (e: Exception) { - if (e is CancellationException) throw e - } - } - - try { - Nip47WalletConnect.parse(uri) - return Route.WalletAddNwc(uri) - } catch (e: Exception) { - if (e is CancellationException) throw e - } - - return null -} - -/** - * Direct route for an `naddr` whose event hasn't arrived in [LocalCache] yet. When a notification - * is tapped (or a `nostr:naddr…` deep link arrives) for a calendar appointment we know is kind - * 31922/31923, route straight to the dedicated detail screen instead of bouncing through - * [Route.EventRedirect]. The detail screen issues its own per-event subscription, so the user - * sees the calendar-specific loading placeholder while the event arrives, not the generic - * redirect screen. - */ -private fun calendarDirectRoute(nip19: NAddress): Route? = - when (nip19.kind) { - CalendarTimeSlotEvent.KIND, - CalendarDateSlotEvent.KIND, - -> Route.CalendarEventDetail(nip19.kind, nip19.author, nip19.dTag) - else -> null - } - -/** - * Direct route for a NIP-29 group `naddr` (kind 39000) shared as a link. The group - * only exists on its host relay, so the naddr's relay hint is mandatory — the group - * id alone can't be resolved. With a hint we open the group chat straight away - * (cold start included); without one there's nowhere to look, so fall through. - */ -private fun relayGroupDirectRoute( - nip19: NAddress, - additionalChars: String?, -): Route? { - if (nip19.kind != GroupMetadataEvent.KIND) return null - val relay = nip19.relay.firstOrNull() ?: return null - // The spec allows an invite code appended as `naddr1…?invite=`; it arrives - // in the trailing chars after the bech32 body. Carry it so the group auto-joins. - val inviteCode = GroupNAddrInvite.parse(additionalChars) - return Route.RelayGroup(nip19.dTag, relay.url, inviteCode = inviteCode) -} - -/** - * Direct route for a NIP-29 group invite link in the de-facto `'[?code=]` - * form shared by Wisp and 0xchat (e.g. `wss://groups.0xchat.com'abc123`). The link carries its - * own host relay, so we can open the group straight away. Returns null for anything that isn't - * this exact shape, so it's safe to try before the nostr-entity parser. - */ -private fun relayGroupInviteRoute(uri: String): Route? { - val link = GroupInviteLink.parse(uri.removePrefix(NOSTR_URI_PREFIX)) ?: return null - return Route.RelayGroup(link.groupId, link.relayUrl.url, inviteCode = link.code) -} - -/** - * A shared Concord invite URL (`…/invite/#`). Cheap substring gates - * keep the parse off the hot path; the whole URL (fragment included) is carried into - * the route so the redeem flow still has the unlock token. - */ -private fun concordInviteRoute(uri: String): Route? = - if (uri.contains("/invite/") && uri.contains('#') && ConcordActions.parseInviteLink(uri) != null) { - Route.ConcordInvite(uri) - } else { - null - } - -/** - * A Buzz workspace invite (`https:///invite/`) — a plain `/invite/` https URL with - * no fragment, so disjoint from the Concord shape above. Opens the in-app join flow - * ([Route.BuzzInvite]) instead of the external browser so the claim signs with the user's key. - */ -private fun buzzInviteRoute(uri: String): Route? = - if (uri.contains("/invite/") && BuzzInviteLink.parse(uri) != null) { - Route.BuzzInvite(uri) - } else { - null - } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index ef3dd4ae99..e093d90651 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -59,7 +59,6 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextDirection @@ -80,6 +79,8 @@ import com.vitorpamplona.amethyst.commons.resources.lightning_invoice import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.components.BechLink import com.vitorpamplona.amethyst.commons.ui.components.OutlinedThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe @@ -102,8 +103,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.playback.composable.VideoView -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription @@ -122,8 +121,6 @@ fun EditPostView( val postViewModel: EditPostViewModel = viewModel() postViewModel.init(accountViewModel) - val context = LocalContext.current - val scrollState = rememberScrollState() val scope = rememberCoroutineScope() @@ -280,7 +277,7 @@ fun EditPostView( accountViewModel.account.settings.defaultFileServer, isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, _, stripMetadata, _ -> - postViewModel.upload(alt, sensitiveContent, mediaQuality, false, server, accountViewModel.toastManager::toast, context, stripMetadata) + postViewModel.upload(alt, sensitiveContent, mediaQuality, false, server, accountViewModel.toastManager::toast, accountViewModel.host.mediaUploader, stripMetadata) accountViewModel.account.settings.changeDefaultFileServer(server) }, onDelete = postViewModel::deleteMediaToUpload, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index 02deee05dc..68377fd17c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.actions -import android.content.Context import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.Stable @@ -41,17 +40,19 @@ import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMediaProcessing +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent @@ -170,11 +171,11 @@ open class EditPostViewModel : ViewModel() { isPrivate: Boolean = false, server: ServerName, onError: (String, String) -> Unit, - context: Context, + uploader: MediaUploader, stripMetadata: Boolean = true, ) { try { - uploadUnsafe(alt, sensitiveContent, mediaQuality, isPrivate, server, onError, context, stripMetadata) + uploadUnsafe(alt, sensitiveContent, mediaQuality, isPrivate, server, onError, uploader, stripMetadata) } catch (e: SignerExceptions.ReadOnlyException) { viewModelScope.launch { onError( @@ -192,7 +193,7 @@ open class EditPostViewModel : ViewModel() { isPrivate: Boolean = false, server: ServerName, onError: (String, String) -> Unit, - context: Context, + uploader: MediaUploader, stripMetadata: Boolean = true, ) { accountViewModel.launchSigner { @@ -206,10 +207,10 @@ open class EditPostViewModel : ViewModel() { myMultiOrchestrator.upload( alt, if (sensitiveContent) "" else null, - MediaCompressor.intToCompressorQuality(mediaQuality), + CompressorQuality.fromSlider(mediaQuality), server, myAccount, - context, + uploader, useH265Codec, stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, @@ -218,11 +219,12 @@ open class EditPostViewModel : ViewModel() { if (results.allGood) { val urls = results.successful.mapNotNull { state -> - if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { + val uploaded = state.result + if (uploaded is UploadOrchestrator.OrchestratorResult.NIP95Result) { val nip95 = myAccount.createNip95( - byteArray = state.result.bytes, - headerInfo = state.result.fileHeader, + byteArray = uploaded.bytes, + headerInfo = uploaded.fileHeader, alt = alt, contentWarningReason = if (sensitiveContent) "" else null, ) @@ -232,29 +234,29 @@ open class EditPostViewModel : ViewModel() { note?.let { "nostr:" + it.toNEvent() } - } else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { + } else if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { val iMeta = - IMetaTagBuilder(state.result.url) + IMetaTagBuilder(uploaded.url) .apply { - hash(state.result.fileHeader.hash) - size(state.result.fileHeader.size) - state.result.fileHeader.mimeType + hash(uploaded.fileHeader.hash) + size(uploaded.fileHeader.size) + uploaded.fileHeader.mimeType ?.let { mimeType(it) } - state.result.fileHeader.dim + uploaded.fileHeader.dim ?.let { dims(it) } - state.result.fileHeader.blurHash + uploaded.fileHeader.blurHash ?.let { blurhash(it.blurhash) } - state.result.fileHeader.thumbHash + uploaded.fileHeader.thumbHash ?.let { thumbhash(it.thumbhash) } - state.result.magnet?.let { magnet(it) } - state.result.uploadedHash?.let { originalHash(it) } + uploaded.magnet?.let { magnet(it) } + uploaded.uploadedHash?.let { originalHash(it) } alt?.let { alt(it) } if (sensitiveContent) sensitiveContent("") }.build() iMetaAttachments = iMetaAttachments.filter { it.url != iMeta.url } + iMeta - state.result.url + uploaded.url } else { null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt index a42bbb8c5c..9ee3618759 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.actions -import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -38,13 +37,15 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_ import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMediaProcessing +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers @@ -100,13 +101,13 @@ open class NewMediaModel : ViewModel() { ): Boolean = mimeType?.startsWith("image/") == true || RichTextParser.isImageUrl(url) fun upload( - context: Context, + uploader: MediaUploader, accountViewModel: AccountViewModel, onSucess: () -> Unit, onError: (String, String) -> Unit, ) { try { - uploadUnsafe(context, accountViewModel, onSucess, onError) + uploadUnsafe(uploader, accountViewModel, onSucess, onError) } catch (e: SignerExceptions.ReadOnlyException) { viewModelScope.launch { onError( @@ -118,7 +119,7 @@ open class NewMediaModel : ViewModel() { } fun uploadUnsafe( - context: Context, + uploader: MediaUploader, accountViewModel: AccountViewModel, onSucess: () -> Unit, onError: (String, String) -> Unit, @@ -135,10 +136,10 @@ open class NewMediaModel : ViewModel() { myMultiOrchestrator.upload( caption, if (sensitiveContent) "" else null, - MediaCompressor.intToCompressorQuality(mediaQualitySlider), + CompressorQuality.fromSlider(mediaQualitySlider), serverToUse, myAccount, - context, + uploader, useH265Codec, stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, 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 9f534be04a..ba41954360 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 @@ -46,7 +46,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -72,20 +71,20 @@ import com.vitorpamplona.amethyst.commons.resources.strip_metadata_label import com.vitorpamplona.amethyst.commons.resources.video_codec_h265_description import com.vitorpamplona.amethyst.commons.resources.video_codec_h265_label import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.ShowImageUploadGallery +import com.vitorpamplona.amethyst.commons.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.SettingSwitchItem +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -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.screen.loggedIn.settings.SettingsRow import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -101,7 +100,6 @@ fun NewMediaView( initialCaption: String = "", ) { val account = accountViewModel.account - val context = LocalContext.current val scrollState = rememberScrollState() @@ -130,7 +128,7 @@ fun NewMediaView( onClose() }, onPost = { - postViewModel.upload(context, accountViewModel, onClose, accountViewModel.toastManager::toast) + postViewModel.upload(accountViewModel.host.mediaUploader, accountViewModel, onClose, accountViewModel.toastManager::toast) postViewModel.selectedServer?.let { account.settings.changeDefaultFileServer(it) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagOutputTransformation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagOutputTransformation.kt index 8306f7eba2..ba59fe6ca9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagOutputTransformation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagOutputTransformation.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.text.style.TextDecoration import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.text.MENTION_REGEX import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull +import com.vitorpamplona.quartz.utils.urldetector.detection.UrlDetector import kotlin.coroutines.cancellation.CancellationException class UrlUserTagOutputTransformation( @@ -76,16 +77,17 @@ class UrlUserTagOutputTransformation( } } +// The same detector the note renderer links with, so the editor highlights what the posted +// note will make clickable. private fun TextFieldBuffer.highlightUrls(color: Color) { val text = asCharSequence().toString() - val urlPattern = android.util.Patterns.WEB_URL - - val matcher = urlPattern.matcher(text) - while (matcher.find()) { - addStyle( - SpanStyle(color = color, textDecoration = TextDecoration.None), - matcher.start(), - matcher.end(), - ) + var searchFrom = 0 + UrlDetector(text).detect().forEach { url -> + val start = text.indexOf(url.originalUrl, searchFrom) + if (start >= 0) { + val end = start + url.originalUrl.length + addStyle(SpanStyle(color = color, textDecoration = TextDecoration.None), start, end) + searchFrom = end + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomImportScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomImportScreen.kt index f6ff467acc..9da5c9ee73 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomImportScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomImportScreen.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.actions.mediaServers -import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -53,7 +52,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp @@ -83,6 +81,7 @@ import com.vitorpamplona.amethyst.commons.resources.blossom_import_sources_secti import com.vitorpamplona.amethyst.commons.resources.blossom_import_start_button import com.vitorpamplona.amethyst.commons.resources.blossom_import_title import com.vitorpamplona.amethyst.commons.resources.delete_media_server +import com.vitorpamplona.amethyst.commons.ui.components.rememberLongNotice import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.pluralStringRes @@ -123,7 +122,7 @@ fun BlossomImportScreen( // kind-10063 list arrives from a relay just after the screen opens (common on cold start). val targetServers by accountViewModel.account.blossomServers.flow .collectAsStateWithLifecycle() - val context = LocalContext.current + val longNotice = rememberLongNotice() Scaffold( topBar = { @@ -254,9 +253,7 @@ fun BlossomImportScreen( when (vm.importSelected()) { is ImportStart.Started -> nav.popBack() ImportStart.Busy -> - Toast - .makeText(context, blossomImportBusyStr, Toast.LENGTH_LONG) - .show() + longNotice.show(blossomImportBusyStr) ImportStart.Empty -> {} } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/FileServerSelectionRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/FileServerSelectionRow.kt index e9cffad67d..cc37db7d72 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/FileServerSelectionRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/FileServerSelectionRow.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.commons.resources.file_server import com.vitorpamplona.amethyst.commons.resources.file_server_description import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsRow import kotlinx.collections.immutable.toImmutableList @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SharedMediaResolver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SharedMediaResolver.kt index 2ff00e88c9..465afd5ee8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SharedMediaResolver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SharedMediaResolver.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.actions.uploads import android.content.Context import androidx.core.net.toUri +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/TakePicture.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/TakePicture.kt index 8597e9dc22..ecade0bf8c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/TakePicture.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/TakePicture.kt @@ -52,6 +52,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.no_camera_app_found import com.vitorpamplona.amethyst.commons.resources.record_a_video import com.vitorpamplona.amethyst.commons.resources.take_a_picture +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/UploadProgressIndicator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/UploadProgressIndicator.kt index 279d636de8..0a8c4439ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/UploadProgressIndicator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/UploadProgressIndicator.kt @@ -49,10 +49,12 @@ import com.vitorpamplona.amethyst.commons.resources.uploading_state_hashing import com.vitorpamplona.amethyst.commons.resources.uploading_state_ready import com.vitorpamplona.amethyst.commons.resources.uploading_state_server_processing import com.vitorpamplona.amethyst.commons.resources.uploading_state_uploading +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.UploadingState import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource @Composable fun UploadProgressIndicator( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallScreen.kt index 93d00acb4d..f91d87f7b2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/call/CallScreen.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.call import android.app.Activity import android.media.projection.MediaProjectionManager -import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background @@ -71,6 +70,7 @@ import com.vitorpamplona.amethyst.commons.resources.call_incoming import com.vitorpamplona.amethyst.commons.resources.call_incoming_video import com.vitorpamplona.amethyst.commons.resources.call_incoming_voice import com.vitorpamplona.amethyst.commons.resources.call_reject +import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.call.session.CallSession @@ -98,7 +98,7 @@ fun CallScreen( val emptyStringFlow = remember { kotlinx.coroutines.flow.MutableStateFlow(null) } val errorMessage by (callSession?.errorMessage ?: emptyStringFlow).collectAsState() - BackHandler(enabled = callState !is CallState.Idle && callState !is CallState.Ended) { + PlatformBackHandler(enabled = callState !is CallState.Idle && callState !is CallState.Ended) { scope.launch { callManager.hangup() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ReusableZapButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ReusableZapButton.kt index 24239668ff..7a2098bbe2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ReusableZapButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ReusableZapButton.kt @@ -39,9 +39,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User @@ -58,21 +56,20 @@ import com.vitorpamplona.amethyst.commons.resources.no_zap_amount_setup_long_pre import com.vitorpamplona.amethyst.commons.resources.thank_you import com.vitorpamplona.amethyst.commons.service.ZapPaymentHandler import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled +import com.vitorpamplona.amethyst.commons.ui.components.ZapButtonCallbacks +import com.vitorpamplona.amethyst.commons.ui.components.ZapButtonConfig import com.vitorpamplona.amethyst.commons.ui.components.toasts.multiline.UserBasedErrorMessage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.ZapIcon import com.vitorpamplona.amethyst.commons.ui.note.ZappedIcon +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.OnchainZapSendDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.navigateToReloadMint import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ModifierWidth3dp -import com.vitorpamplona.amethyst.commons.ui.theme.Size14Modifier -import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier -import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon import com.vitorpamplona.amethyst.ui.note.ZapAmountChoicePopup import com.vitorpamplona.amethyst.ui.note.payViaIntent -import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.OnchainZapSendDialog import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList @@ -83,35 +80,14 @@ import kotlinx.coroutines.launch import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid -/** - * Configuration for zap button behavior and appearance - */ -data class ZapButtonConfig( - val grayTint: Color = Color.Gray, - val iconSize: Dp = Size35dp, - val iconSizeModifier: Modifier = Size20Modifier, - val animationModifier: Modifier = Size14Modifier, - val showUserFinderSubscription: Boolean = false, - val zapAmountChoices: List? = null, - val thankYouText: String? = null, - val buttonText: String? = null, -) - -/** - * Callbacks for zap button events - */ -data class ZapButtonCallbacks( - val onZapComplete: ((Boolean) -> Unit)? = null, -) - @OptIn(ExperimentalUuidApi::class) @Composable fun ReusableZapButton( baseNote: Note, accountViewModel: AccountViewModel, nav: INav, - config: ZapButtonConfig = ZapButtonConfig(), - callbacks: ZapButtonCallbacks = ZapButtonCallbacks(), + config: ZapButtonConfig, + callbacks: ZapButtonCallbacks, ) { val noWalletFoundStr = stringRes(Res.string.no_wallet_found) var wantsToZap by remember { mutableStateOf?>(null) } @@ -297,8 +273,7 @@ fun ReusableZapButton( val displayText = when { hasZapped -> config.thankYouText ?: stringRes(id = Res.string.thank_you) - config.buttonText != null -> config.buttonText - else -> stringRes(id = Res.string.donate_now) + else -> config.buttonText ?: stringRes(id = Res.string.donate_now) } Text(text = displayText) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ThinPaddingTextField.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ThinPaddingTextField.kt index e1caaddd21..00bd0ac935 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ThinPaddingTextField.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ThinPaddingTextField.kt @@ -20,14 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.components -import android.net.Uri import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.content.MediaType -import androidx.compose.foundation.content.ReceiveContentListener -import androidx.compose.foundation.content.TransferableContent -import androidx.compose.foundation.content.consume -import androidx.compose.foundation.content.contentReceiver -import androidx.compose.foundation.content.hasMediaType import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.PaddingValues @@ -60,6 +53,8 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri +import com.vitorpamplona.amethyst.commons.ui.components.imageContentReceiver import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText // COPIED FROM TEXT FIELD @@ -72,7 +67,7 @@ fun ThinPaddingTextField( state: TextFieldState, modifier: Modifier = Modifier, onTextChanged: (() -> Unit)? = null, - onContentReceived: ((Uri, String?) -> Unit)? = null, + onContentReceived: ((MediaUri, String?) -> Unit)? = null, enabled: Boolean = true, readOnly: Boolean = false, textStyle: TextStyle = LocalTextStyle.current, @@ -142,30 +137,7 @@ fun ThinPaddingTextField( val contentModifier = if (onContentReceived != null) { - modifier.contentReceiver( - object : ReceiveContentListener { - override fun onReceive(transferableContent: TransferableContent): TransferableContent? { - if (!transferableContent.hasMediaType(MediaType.Image)) { - return transferableContent - } - val remaining = - transferableContent.consume { item -> - val uri = item.uri - if (uri != null) { - onContentReceived( - uri, - transferableContent.clipEntry.clipData.description - .getMimeType(0), - ) - true - } else { - false - } - } - return remaining - } - }, - ) + modifier.imageContentReceiver(onContentReceived) } else { modifier } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt index f332aa5ba3..4054d39704 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt @@ -39,6 +39,7 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.ui.components.DisplayFullNote import com.vitorpamplona.amethyst.commons.ui.components.DisplayUser +import com.vitorpamplona.amethyst.commons.ui.fragmentHashtagOrNull import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.richtext.HashtagIcon import com.vitorpamplona.amethyst.commons.ui.richtext.checkForHashtagWithIcon @@ -48,7 +49,6 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.LoadedBechLink import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview import com.vitorpamplona.amethyst.ui.components.ZoomableContentView -import com.vitorpamplona.amethyst.ui.fragmentHashtagOrNull import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt index ba24f0c0bd..3c0ebff2d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt @@ -49,10 +49,10 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.theme.MarkdownTextStyle import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.ui.theme.markdownStyle +import com.vitorpamplona.amethyst.commons.ui.uriToRoute import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.model.UrlCachedPreviewer -import com.vitorpamplona.amethyst.ui.uriToRoute import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip92IMeta.imetasByUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 2531c35626..b7271fb601 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -59,75 +59,153 @@ import com.vitorpamplona.amethyst.commons.relayClient.user.LocalUserFinderAccoun import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.invalid_nip19_uri import com.vitorpamplona.amethyst.commons.resources.invalid_nip19_uri_description +import com.vitorpamplona.amethyst.commons.ui.actions.NewUserMetadataScreen import com.vitorpamplona.amethyst.commons.ui.layouts.LocalScreenLayout import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.LocalTabReselectCoordinator import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.TabReselectCoordinator +import com.vitorpamplona.amethyst.commons.ui.navigation.findQueryParameterValue import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.apps.recommendations.ProfileAppRecommendationsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.articles.ArticlesScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups.BackupConflictReviewScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges.BadgesScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges.award.AwardBadgeScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges.profile.ProfileBadgesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.default.BookmarkListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.list.ListOfBookmarkGroupsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.membershipManagement.ArticleBookmarkListManagementScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.membershipManagement.PostBookmarkListManagementScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.old.OldBookmarkListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.podcasts.BookmarkedPodcastsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.repositories.BookmarkedRepositoriesScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.browser.BrowserScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.AgentAttestationScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.AgentConsoleScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.AgentPersonaEditScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.AgentWorkBoardScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzCanvasScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzDmListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzForumPostScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzInviteScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzNewDmScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.JobBoardScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.WorkflowRunBoardScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnGroupInfoScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnInvitationsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.geohashChat.GeohashTeleportScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordChannelListScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordCreateScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordEditScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordHomeScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordInviteLinksScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordInviteScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordMembersScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupBrowseScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupDiscoveryScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupMembersScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupThreadsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.CommunityScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.list.CommunitiesScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.newCommunity.EditCommunityScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.newCommunity.NewCommunityScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.contactList.ContactListUsersScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.drafts.DraftListScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.dvms.DvmContentDiscoveryScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.dvms.favorites.FavoriteAlgoFeedsListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.emojipacks.browse.BrowseEmojiSetsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.emojipacks.display.EmojiPackScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.emojipacks.list.ListOfEmojiPacksScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.emojipacks.list.metadata.EmojiPackMetadataScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.emojipacks.membershipManagement.EmojiPackSelectionScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.emojipacks.membershipManagement.MyEmojiListScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.favorites.FavoriteAppsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.followPacks.list.FollowPacksScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.hunt.GeocacheHuntScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.hunt.NewGeocacheHuntScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.log.LogGeocacheFindScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geohash.GeoHashScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.gitRepositories.GitRepositoriesScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag.HashtagScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.highlights.HighlightsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.interestSets.display.InterestSetScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.interestSets.list.ListOfInterestSetsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.interestSets.list.metadata.InterestSetMetadataScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.display.lists.PeopleListScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.display.packs.FollowPackScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.list.ListOfPeopleListsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.memberEdit.FollowListAndPackAndUserScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.livestreams.LiveStreamsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.MusicPlaylistsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.MusicTracksScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.NewMusicPlaylistScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.napplets.ConnectedAppsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.newUser.ImportFollowListPickFollowsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.newUser.ImportFollowListSelectUserScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.nsites.NsitesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.pinnednotes.PinnedNotesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.PodcastEpisodesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.PodcastsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.EditPodcastShowScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.NewPodcastTrailerScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.PodcastAuthoringScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls.PollsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls.results.PollResultsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.privacy.PrivacyOptionsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.products.ProductsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.publicChats.PublicChatsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.ShowQRScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.redirect.LoadRedirectScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relay.RelayFeedScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relayauth.RelayAuthSettingsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.RelayInformationScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.eventsync.EventSyncScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.nip43.RelayMembersScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.vanish.VanishEventsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.AllSettingsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.AudioVisualizerSettingsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.BlockedUsersScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.BottomBarSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.CallSettingsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.DrawerSettingsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.HiddenWordsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.HomeTabsSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.MessagesSettingsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.MutedThreadsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.ProfileUiSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.ReactionsSettingsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SecurityFiltersScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SpammingUsersScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.VideoPlayerSettingsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.CordnCoordinatorsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.CordnHubScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.CordnKeyPackagesScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.CordnLinkScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.nip46.Nip46SignerScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.softwareapps.SoftwareAppDetailScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.softwareapps.SoftwareAppsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.AddClinkDebitWalletScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.AddNwcWalletScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.AddWalletScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.CashuMintRecommendationsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.CashuMintsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.CashuWalletScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.CashuWalletSettingsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.OnchainTransactionsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.ReloadMintScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.TopUpMintScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.WalletDetailScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.WalletReceiveScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.WalletScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.WalletSendScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.WalletTransactionsScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.wizard.CashuWalletWizardScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.workouts.WorkoutsScreen +import com.vitorpamplona.amethyst.commons.ui.uriToRoute import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.crashreports.DisplayCrashMessages import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.RelayAuthPromptHost import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.compose.DisplayNotifyMessages import com.vitorpamplona.amethyst.service.resourceusage.DisplayResourceUsageAlert import com.vitorpamplona.amethyst.service.resourceusage.ScreenTimeIntegrator -import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataScreen import com.vitorpamplona.amethyst.ui.actions.bolt12Offers.Bolt12OffersScreen import com.vitorpamplona.amethyst.ui.actions.mediaServers.AllMediaServersScreen import com.vitorpamplona.amethyst.ui.actions.mediaServers.BlossomBlobManagerScreen @@ -152,24 +230,10 @@ import com.vitorpamplona.amethyst.ui.note.share.ShareNoteAsImageScreen import com.vitorpamplona.amethyst.ui.note.share.ShareNoteAsQrScreen import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout -import com.vitorpamplona.amethyst.ui.screen.loggedIn.articles.ArticlesScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.backups.BackupConflictReviewScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.BadgesScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.award.AwardBadgeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.display.BookmarkGroupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.list.metadata.BookmarkGroupMetadataScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.old.OldBookmarkListScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.BrowserScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.WebAppScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.AgentAttestationScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.AgentConsoleScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.AgentWorkBoardScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzCanvasScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzDmListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzForumThreadScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzInviteScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.JobBoardScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.WorkflowRunBoardScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarCollectionsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarReminderSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsScreen @@ -179,7 +243,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.detail.CalendarEv import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnCreateGroupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnCreateMembersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnGroupChatScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnGroupInfoScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnGroupListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.geohashChat.GeohashChatScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.geohashChat.GeohashChatsScreen @@ -193,18 +256,12 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.minichat.MinichatScre import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomByAuthorScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.NewGroupDMScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.ConcordChannelListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.ConcordChannelScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.ConcordCreateScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.ConcordEditScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.ConcordHomeScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.ConcordInviteLinksScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.EphemeralChatScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.metadata.NewEphemeralChatScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata.ChannelMetadataScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivityChannelScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupBrowseScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChannelListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChatScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupCreateScreen @@ -213,55 +270,38 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.MessagesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.share.ShareToDMScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessGameScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessLobbyScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.list.CommunitiesScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.newCommunity.EditCommunityScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.newCommunity.NewCommunityScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.LongFormPostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.NewProductScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.DvmContentDiscoveryScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.favorites.FavoriteAlgoFeedsListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabAccountWatcher import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabLayer import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabPreloader import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabThemeWatcher import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.FavoriteAppManifestPreloader -import com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.display.EmojiPackScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.list.metadata.EmojiPackMetadataScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.favorites.FavoriteAppsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.favorites.NostrAppScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.FollowPackFeedScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.list.FollowPacksScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.GeocachesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.create.NewGeocacheScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.detail.GeocacheDetailScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.log.LogGeocacheFindScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashPostScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitNewIssueScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryCodeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryIssuesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryPullsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagPostScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.highlights.NewHighlightScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.HomeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.VoiceReplyScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.nip75Goals.NewGoalScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.AccountBackupScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.lists.PeopleListScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.packs.FollowPackScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata.FollowPackMetadataScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.metadata.PeopleListMetadataScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.LiveStreamsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.LongsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.AddToMusicPlaylistSheet -import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.NewMusicPlaylistScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.NewMusicTrackScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.ConnectedAppDetailScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.ConnectedAppsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.NappletsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.NestsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.lobby.NestLobbyScreen @@ -269,78 +309,39 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationS import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages.NewPublicMessageScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.PicturesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.PodcastScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring.EditPodcastShowScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring.NewPodcastEpisodeScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring.NewPodcastTrailerScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.PollPostScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.privacy.PrivacyOptionsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.products.ProductsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.payment.SendPaymentScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.publicChats.PublicChatsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ScanQrImageScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ShowQRScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.redirect.LoadRedirectScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relay.RelayFeedScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relayauth.RelayAuthSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.AllRelayListScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip86.RelayManagementScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.subscriptions.ActiveSubscriptionsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.vanish.RequestToVanishScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.scheduledposts.ScheduledPostsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.AllSettingsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.AudioVisualizerSettingsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.BlockedUsersScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.BottomBarSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.ComposeSettingsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.DrawerSettingsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.HiddenWordsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.HomeTabsSettingsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.MutedThreadsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NIP47SetupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NamecoinSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NotificationSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.OtsSettingsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.ProfileUiSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.ResourceUsageScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SecurityFiltersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SpammingUsersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.UpdateZapAmountScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.UserSettingsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.VideoPlayerSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.CordnBackupScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.CordnCoordinatorsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.CordnHubScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.CordnKeyPackagesScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.CordnLinkScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.CordnMigrateScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46ConnectedAppsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46SignerScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.ShortsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.softwareapps.SoftwareAppDetailScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.ThreadScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.url.UrlPostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.url.UrlScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.VideoScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.hls.NewHlsVideoScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.AddClinkDebitWalletScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.AddNwcWalletScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.CashuWalletScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.OnchainTransactionsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletDetailScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletReceiveScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletSendScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletTransactionsScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.wizard.CashuWalletWizardScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.webBookmarks.WebBookmarksScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.NewWorkoutScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.fitness.MyFitnessScreen import com.vitorpamplona.amethyst.ui.screen.loggedOff.AddAccountDialog -import com.vitorpamplona.amethyst.ui.uriToRoute import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip84Highlights.parse.SharedHighlightParser @@ -1337,34 +1338,3 @@ fun URI.findParameterValue(parameterName: String): String? = Pair(name, value) }?.firstOrNull { it.first == parameterName } ?.second - -/** - * The value of [parameterName] in this URI's query string, without going through - * [java.net.URI]. - * - * Needed because `java.net.URI` only exposes `rawQuery` for *hierarchical* URIs. A URI - * with a scheme and no `//` is **opaque** — everything after the colon is one - * scheme-specific part — so `URI("marmot:?account=npub1…").rawQuery` is null. That - * is the shape [com.vitorpamplona.amethyst.service.notifications.NotificationRoutes.marmotUri] - * produces, so every Marmot group notification silently lost its `?account=` and opened - * the group under whichever account happened to be current instead of switching first. - * - * Splitting on the first `?` gets the same answer for both shapes, and returns null for a - * bare `nevent1…` with no query at all. - * - * [com.vitorpamplona.quartz.utils.UriParser] reads an opaque query correctly too, and is the - * right tool when a URI is already known to be well-formed. It is not this one: it builds a - * [java.net.URI], which *throws* on anything that is not a legal URI. What arrives here comes - * from an exported, browsable scheme, so it can be any string at all, and every caller on the - * deep-link path treats an unreadable uri as "no route" rather than as a crash. - */ -fun String.findQueryParameterValue(parameterName: String): String? { - val query = substringAfter('?', "") - if (query.isEmpty()) return null - - return query - .split('&') - .firstOrNull { it.substringBefore('=') == parameterName } - ?.substringAfter('=', "") - ?.ifEmpty { null } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppNavigationRail.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppNavigationRail.kt index d34b55cf9c..53b0200551 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppNavigationRail.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppNavigationRail.kt @@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.navigation.BottomBarEntry import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.LocalTabReselectCoordinator +import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.rememberBottomBarSlot import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.LoggedInUserPictureDrawer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.navigation.navs.Nav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt index 757f7ebc0c..e947d9f041 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt @@ -127,6 +127,11 @@ import com.vitorpamplona.amethyst.commons.resources.status_update import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPostStatus import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.layouts.PermanentDrawerWidth +import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.NavBarCatalog +import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.NavBarItemDef +import com.vitorpamplona.amethyst.commons.ui.navigation.drawer.DrawerSection +import com.vitorpamplona.amethyst.commons.ui.navigation.drawer.DrawerSectionVisibility +import com.vitorpamplona.amethyst.commons.ui.navigation.drawer.DrawerSections import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor @@ -157,8 +162,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.profileContentHeaderModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.isDebug -import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarCatalog -import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarItemDef import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt index 41780eb4e3..0bdf70ceac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.navigation.navs -import android.annotation.SuppressLint import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerValue import androidx.compose.runtime.Composable @@ -272,7 +271,7 @@ class Nav( } } - @SuppressLint("RestrictedApi") + @Suppress("RestrictedApi") override fun popUpTo( route: Route, klass: KClass, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/AroundMeLocationLabel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/AroundMeLocationLabel.kt index 9ebc663f23..dae3a52c12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/AroundMeLocationLabel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/AroundMeLocationLabel.kt @@ -40,10 +40,10 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.lack_location_permissions import com.vitorpamplona.amethyst.commons.resources.loading_location import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Font12SP import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer -import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName /** The "around me" feed's subtitle: asks for location, then shows the city it resolved to. */ @OptIn(ExperimentalPermissionsApi::class) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/LocalizedDateTimeFormat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/LocalizedDateTimeFormat.kt index 70ecc2dc71..e58e919b6b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/LocalizedDateTimeFormat.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/LocalizedDateTimeFormat.kt @@ -22,40 +22,12 @@ package com.vitorpamplona.amethyst.ui.note import android.content.Context import android.text.format.DateFormat -import java.text.SimpleDateFormat import java.util.Date -import java.util.Locale -/** - * Date/time formatters that respect the user's Android system settings: - * - Date order (dd/mm/yyyy, mm/dd/yyyy, yyyy-mm-dd…) is derived from the - * active [Locale] via [DateFormat.getBestDateTimePattern]. - * - 12-hour vs 24-hour clock follows the system setting via - * [DateFormat.is24HourFormat] (which is the user's manual override on - * top of the locale default). - * - * All formatters here use Unicode LDML skeletons. `j` and `jm` are intentionally - * avoided — `j` only picks 12/24 hour from the *locale*, not the user override. - * We pick the time skeleton explicitly based on [DateFormat.is24HourFormat]. - */ - -private fun timeSkeleton(context: Context): String = if (DateFormat.is24HourFormat(context)) "Hm" else "hma" - -private fun bestPattern( - context: Context, - skeletonBase: String, - includeTime: Boolean, -): SimpleDateFormat { - val locale = Locale.getDefault() - val skeleton = if (includeTime) skeletonBase + timeSkeleton(context) else skeletonBase - return SimpleDateFormat(DateFormat.getBestDateTimePattern(locale, skeleton), locale) -} - -/** Locale-aware month + day + short time (e.g. "May 28, 14:32" / "28 May, 2:32 PM"). */ -fun formatMonthDayTime( - epochSeconds: Long, - context: Context, -): String = bestPattern(context, "MMMd", includeTime = true).format(Date(epochSeconds * 1000L)) +// Date/time formatters that respect the user's Android system settings: the date order comes +// from the active locale and the clock follows DateFormat.is24HourFormat (the user's manual +// override on top of the locale default). The shared month/day/time formatter lives in +// commonsUI (rememberMonthDayTimeFormatter). /** Locale-aware medium date (e.g. "May 28, 2026" / "28 May 2026" / "28.05.2026"). */ fun formatMediumDate( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 7bda67d532..88beb7be95 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -195,6 +195,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.ZappedIcon import com.vitorpamplona.amethyst.commons.ui.note.elements.ShareOptionsBottomSheet import com.vitorpamplona.amethyst.commons.ui.note.types.EditState import com.vitorpamplona.amethyst.commons.ui.richtext.InLineIconRenderer +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.OnchainZapSendDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.navigateToReloadMint import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange @@ -243,7 +244,6 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.PaymentTargetsDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.paymentTargetStyleFor -import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.OnchainZapSendDialog import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent 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 f1261acfd5..f717e04ab5 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 @@ -109,6 +109,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.commons.ui.note.DisplayBlankAuthor import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.commons.ui.note.buttons.CloseButton +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.OnchainZapSendDialog import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer @@ -119,7 +120,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ZeroPadding import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.OnchainZapSendDialog import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt index 3c98337d8e..7ec5a1187d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.creators.polls -import android.annotation.SuppressLint import android.text.format.DateFormat import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -164,7 +163,7 @@ fun PollDeadlinePicker(model: ShortNotePostViewModel) { } } -@SuppressLint("ViewModelConstructorInComposable") +@Suppress("ViewModelConstructorInComposable") @Preview @Composable fun PollDeadlinePickerPreview() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollOptionsField.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollOptionsField.kt index 2a89c7ef1c..abb263f2dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollOptionsField.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollOptionsField.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.creators.polls -import android.annotation.SuppressLint import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -154,7 +153,7 @@ fun PollTypeSelector( } } -@SuppressLint("ViewModelConstructorInComposable") +@Suppress("ViewModelConstructorInComposable") @Preview @Composable fun NewPollClosingPreview() { 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 edd9282f5b..10d312c1a5 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 @@ -90,6 +90,9 @@ import com.vitorpamplona.amethyst.commons.resources.strip_metadata_description import com.vitorpamplona.amethyst.commons.resources.strip_metadata_label import com.vitorpamplona.amethyst.commons.resources.video_codec_h265_description import com.vitorpamplona.amethyst.commons.resources.video_codec_h265_label +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMediaProcessing +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.ShowImageUploadGallery import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer import com.vitorpamplona.amethyst.commons.ui.note.CancelIcon @@ -102,9 +105,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.ai.MLKitImageLabelService -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing -import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery import kotlinx.collections.immutable.toImmutableList @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollConsensusThreshold.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollConsensusThreshold.kt index d8dc0633d4..c05c747b13 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollConsensusThreshold.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollConsensusThreshold.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.creators.zappolls -import android.annotation.SuppressLint import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -104,7 +103,7 @@ fun ZapPollConsensusThreshold(pollViewModel: ShortNotePostViewModel) { } } -@SuppressLint("ViewModelConstructorInComposable") +@Suppress("ViewModelConstructorInComposable") @Preview @Composable fun ZapPollConsensusThresholdPreview() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt index d53f4c10e1..a961fd4d82 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.creators.zappolls -import android.annotation.SuppressLint import android.text.format.DateFormat import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -164,7 +163,7 @@ fun ZapPollDeadlinePicker(model: ShortNotePostViewModel) { } } -@SuppressLint("ViewModelConstructorInComposable") +@Suppress("ViewModelConstructorInComposable") @Preview @Composable fun ZapPollDeadlinePickerPreview() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollOption.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollOption.kt index 3bc9882b5e..aa66a0e2a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollOption.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollOption.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.creators.zappolls -import android.annotation.SuppressLint import androidx.compose.foundation.layout.Row import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.IconButton @@ -89,7 +88,7 @@ fun ZapPollOption( } } -@SuppressLint("ViewModelConstructorInComposable") +@Suppress("ViewModelConstructorInComposable") @Preview @Composable fun ZapPollOptionPreview() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollVoteValueRange.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollVoteValueRange.kt index 884e372553..14787eedab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollVoteValueRange.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollVoteValueRange.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.creators.zappolls -import android.annotation.SuppressLint import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -120,7 +119,7 @@ fun ZapPollVoteValueRange(pollViewModel: ShortNotePostViewModel) { } } -@SuppressLint("ViewModelConstructorInComposable") +@Suppress("ViewModelConstructorInComposable") @Preview @Composable fun ZapPollVoteValueRangePreview() { 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 fe18936d50..bb649a217a 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 @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.nip22Comments -import android.content.Context import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd @@ -55,6 +54,12 @@ import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMediaProcessing +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.IAudience @@ -66,13 +71,9 @@ import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.text.setTextAndPlaceCursorAtBeginning +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.location.LocationState -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor @@ -787,11 +788,11 @@ open class CommentPostViewModel : mediaQuality: Int, server: ServerName, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, stripMetadata: Boolean = true, ) { try { - uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, context, stripMetadata) + uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, uploader, stripMetadata) } catch (_: SignerExceptions.ReadOnlyException) { viewModelScope.launch { onError( @@ -808,7 +809,7 @@ open class CommentPostViewModel : mediaQuality: Int, server: ServerName, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, stripMetadata: Boolean = true, ) { viewModelScope.launch(Dispatchers.IO) { @@ -820,10 +821,10 @@ open class CommentPostViewModel : myMultiOrchestrator.upload( alt, contentWarningReason, - MediaCompressor.intToCompressorQuality(mediaQuality), + CompressorQuality.fromSlider(mediaQuality), server, account, - context, + uploader, stripMetadata = stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, forcedSigner = if (wantsAnonymousPost) anonymousSigner() else null, @@ -832,30 +833,31 @@ open class CommentPostViewModel : if (results.allGood) { val urls = results.successful.mapNotNull { state -> - if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { - val nip95 = account.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason) + val uploaded = state.result + if (uploaded is UploadOrchestrator.OrchestratorResult.NIP95Result) { + val nip95 = account.createNip95(uploaded.bytes, headerInfo = uploaded.fileHeader, alt, contentWarningReason) nip95attachments = nip95attachments + nip95 val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) } note?.let { "nostr:" + it.toNEvent() } - } else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { + } else if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { val iMeta = - IMetaTagBuilder(state.result.url) + IMetaTagBuilder(uploaded.url) .apply { - hash(state.result.fileHeader.hash) - size(state.result.fileHeader.size) - state.result.fileHeader.mimeType + hash(uploaded.fileHeader.hash) + size(uploaded.fileHeader.size) + uploaded.fileHeader.mimeType ?.let { mimeType(it) } - state.result.fileHeader.dim + uploaded.fileHeader.dim ?.let { dims(it) } - state.result.fileHeader.blurHash + uploaded.fileHeader.blurHash ?.let { blurhash(it.blurhash) } - state.result.fileHeader.thumbHash + uploaded.fileHeader.thumbHash ?.let { thumbhash(it.thumbhash) } - state.result.magnet?.let { magnet(it) } - state.result.uploadedHash?.let { originalHash(it) } + uploaded.magnet?.let { magnet(it) } + uploaded.uploadedHash?.let { originalHash(it) } alt?.let { alt(it) } contentWarningReason?.let { sensitiveContent(contentWarningReason) } @@ -863,7 +865,7 @@ open class CommentPostViewModel : iMetaAttachments.replace(iMeta.url, iMeta) - state.result.url + uploaded.url } else { null } 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 387ff21249..484b7aa166 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 @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.note.nip22Comments import android.content.Intent import android.net.Uri -import androidx.activity.compose.BackHandler import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Box @@ -79,6 +78,10 @@ import com.vitorpamplona.amethyst.commons.resources.post_anonymously import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind import com.vitorpamplona.amethyst.commons.resources.zapraiser import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery +import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.BaseUserPicture @@ -95,6 +98,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraise import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapToButton import com.vitorpamplona.amethyst.commons.ui.note.nip22Comments.DisplayExternalId +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.ui.note.types.ReplyRenderType import com.vitorpamplona.amethyst.commons.ui.platform.GeohashLocationPickerDialog import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -107,9 +111,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.components.getActivity @@ -118,7 +119,6 @@ import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoad import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.location.GeoHashPostSection -import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField import com.vitorpamplona.amethyst.ui.note.creators.notify.AudienceFlap import com.vitorpamplona.amethyst.ui.note.creators.notify.AudienceSheet @@ -227,7 +227,7 @@ fun GenericCommentPostScreen( StrippingFailureDialog(postViewModel.strippingFailureConfirmation) - BackHandler { + PlatformBackHandler { accountViewModel.launchSigner { postViewModel.sendDraftSync() onUiThread { postViewModel.cancel() } @@ -472,7 +472,7 @@ private fun GenericCommentPostBody( accountViewModel.account.settings.defaultFileServer, isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, _, stripMetadata, _ -> - postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context, stripMetadata) + postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, accountViewModel.host.mediaUploader, stripMetadata) accountViewModel.account.settings.changeDefaultFileServer(server) }, onDelete = postViewModel::deleteMediaToUpload, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/AndroidNotePlatform.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/AndroidNotePlatform.kt index ff1a900619..f887f9ff58 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/AndroidNotePlatform.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/platform/AndroidNotePlatform.kt @@ -34,6 +34,8 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewState +import com.vitorpamplona.amethyst.commons.ui.components.ZapButtonCallbacks +import com.vitorpamplona.amethyst.commons.ui.components.ZapButtonConfig import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.platform.NotePlatform import com.vitorpamplona.amethyst.commons.ui.note.types.EditState @@ -50,6 +52,7 @@ import com.vitorpamplona.amethyst.service.playback.composable.VideoView as AppVi import com.vitorpamplona.amethyst.ui.actions.EditPostView as AppEditPostView import com.vitorpamplona.amethyst.ui.components.GifVideoView as AppGifVideoView import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview as AppLoadUrlPreview +import com.vitorpamplona.amethyst.ui.components.ReusableZapButton as AppReusableZapButton import com.vitorpamplona.amethyst.ui.components.ZoomableContentView as AppZoomableContentView import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog as AppZoomableImageDialog import com.vitorpamplona.amethyst.ui.components.rememberUrlPreviewState as AppRememberUrlPreviewState @@ -584,6 +587,15 @@ object AndroidNotePlatform : NotePlatform { nav = nav, ) + @Composable + override fun ReusableZapButton( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, + config: ZapButtonConfig, + callbacks: ZapButtonCallbacks, + ) = AppReusableZapButton(baseNote, accountViewModel, nav, config, callbacks) + @Composable override fun EditPostView( onClose: () -> Unit, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt index 74a9cee072..a218c7d509 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt @@ -90,6 +90,9 @@ import com.vitorpamplona.amethyst.commons.resources.share_as_image import com.vitorpamplona.amethyst.commons.resources.share_as_image_generating import com.vitorpamplona.amethyst.commons.resources.share_as_image_url import com.vitorpamplona.amethyst.commons.resources.share_as_image_watermark +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.ui.components.LoadNote import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer @@ -98,10 +101,8 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackBu import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size18Modifier +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -268,7 +269,7 @@ fun ShareNoteAsImageScreen( compressionQuality = CompressorQuality.UNCOMPRESSED, server = server, account = account, - context = context, + uploader = accountViewModel.host.mediaUploader, stripMetadata = false, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsQrScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsQrScreen.kt index 9044bcecb9..0932556399 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsQrScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsQrScreen.kt @@ -61,10 +61,10 @@ import com.vitorpamplona.amethyst.commons.resources.share_as_qr_mode_web import com.vitorpamplona.amethyst.commons.ui.components.LoadNote import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.KeepScreenBrightAndAwake +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.KeepScreenBrightAndAwake -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer // A cap, not a fixed size: QrCodeDrawer's own quiet zone (QR_QUIET_ZONE_MODULES in QrCodeDrawer.kt) is a // fixed pixel count subtracted from raw size.width, so its share of the tile grows as density diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/StaticWebsite.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/StaticWebsite.kt index f8c8cf0b36..093a411218 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/StaticWebsite.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/StaticWebsite.kt @@ -24,11 +24,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalContext import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.favorites.favoriteCoordinateOf import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.StaticWebsiteCard import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher import com.vitorpamplona.amethyst.napplet.NappletLauncher import com.vitorpamplona.amethyst.napplethost.HostProfile import com.vitorpamplona.amethyst.napplethost.NappletBlobPrefetcher @@ -68,7 +68,7 @@ fun RenderRootNappletEvent( } else { null }, - headerActions = { FavoriteToggleButton(FavoriteAppLauncher.coordinateOf(event), event.title() ?: "Napplet", event.icon()) }, + headerActions = { FavoriteToggleButton(favoriteCoordinateOf(event), event.title() ?: "Napplet", event.icon()) }, ) } @@ -98,7 +98,7 @@ fun RenderNamedNappletEvent( } else { null }, - headerActions = { FavoriteToggleButton(FavoriteAppLauncher.coordinateOf(event), event.title() ?: event.identifier(), event.icon()) }, + headerActions = { FavoriteToggleButton(favoriteCoordinateOf(event), event.title() ?: event.identifier(), event.icon()) }, ) } @@ -139,7 +139,7 @@ fun RenderRootSiteEvent( } else { null }, - headerActions = { FavoriteToggleButton(FavoriteAppLauncher.coordinateOf(event), event.title() ?: "nsite", event.icon()) }, + headerActions = { FavoriteToggleButton(favoriteCoordinateOf(event), event.title() ?: "nsite", event.icon()) }, ) } @@ -180,7 +180,7 @@ fun RenderNamedSiteEvent( } else { null }, - headerActions = { FavoriteToggleButton(FavoriteAppLauncher.coordinateOf(event), event.title() ?: event.identifier(), event.icon()) }, + headerActions = { FavoriteToggleButton(favoriteCoordinateOf(event), event.title() ?: event.identifier(), event.icon()) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppPlatform.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppPlatform.kt index 47ad8b75a9..468d992788 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppPlatform.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppPlatform.kt @@ -20,19 +20,41 @@ */ package com.vitorpamplona.amethyst.ui.platform +import android.content.Context import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.UriHandler +import com.vitorpamplona.amethyst.BuildConfig +import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.platform.AppLauncher import com.vitorpamplona.amethyst.commons.ui.platform.AppPlatform +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.ScanOutcome +import com.vitorpamplona.amethyst.commons.ui.settings.SettingsCategory import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher import com.vitorpamplona.amethyst.ui.navigation.topbars.AndroidAroundMeLocationLabel -import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar as AppBottomBarImpl +import com.vitorpamplona.quartz.concord.cord02Community.ImagePointer +import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.AppBottomBar as AppBottomBarImpl +import com.vitorpamplona.amethyst.favorites.rememberManifestIconModel as AppRememberManifestIconModel +import com.vitorpamplona.amethyst.favorites.rememberNappletIconModel as AppRememberNappletIconModel +import com.vitorpamplona.amethyst.favorites.rememberWebAppIconModel as AppRememberWebAppIconModel import com.vitorpamplona.amethyst.ui.note.creators.location.GeohashLocationPickerContent as AppGeohashLocationPickerContent import com.vitorpamplona.amethyst.ui.note.creators.location.GeohashLocationPickerDialog as AppGeohashLocationPickerDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.rememberConcordImageModel as AppRememberConcordImageModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.scanner.QrCodeScannerDialog as AppQrCodeScannerDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.legalSettingsCategory as flavorLegalSettingsCategory -/** Android's [AppPlatform]: the app's own bottom bar and location label. */ +/** Android's [AppPlatform]: the app's own shell pieces, camera scanner, app launcher and icon caches. */ object AndroidAppPlatform : AppPlatform { + override val isCastingAvailable: Boolean get() = BuildConfig.IS_CASTING_AVAILABLE + + // Per flavour: Play links the hosted policies, F-Droid surfaces none. + override fun legalSettingsCategory(uriHandler: UriHandler): SettingsCategory? = flavorLegalSettingsCategory(uriHandler) + @Composable override fun AppBottomBar( selectedRoute: Route?, @@ -58,4 +80,48 @@ object AndroidAppPlatform : AppPlatform { onConfirm: (String) -> Unit, modifier: Modifier, ) = AppGeohashLocationPickerContent(initialGeohash, confirmLabel, onConfirm, modifier) + + @Composable + override fun QrCodeScannerDialog( + onDismiss: () -> Unit, + onScan: (String) -> ScanOutcome, + ) = AppQrCodeScannerDialog(onDismiss, onScan) + + @Composable + override fun rememberAppLauncher(): AppLauncher { + val context = LocalContext.current + return remember(context) { AndroidAppLauncher(context) } + } + + @Composable + override fun rememberConcordImageModel( + pointer: ImagePointer?, + accountViewModel: AccountViewModel, + ): String? = AppRememberConcordImageModel(pointer, accountViewModel) + + @Composable + override fun rememberNappletIconModel(coordinate: String): String? = AppRememberNappletIconModel(coordinate) + + @Composable + override fun rememberManifestIconModel( + author: String, + identifier: String, + ): String? = AppRememberManifestIconModel(author, identifier) + + @Composable + override fun rememberWebAppIconModel(url: String): String? = AppRememberWebAppIconModel(url) +} + +private class AndroidAppLauncher( + private val context: Context, +) : AppLauncher { + override fun launch( + app: FavoriteApp, + stillLoading: String, + ) = FavoriteAppLauncher.launch(context, app, stillLoading) + + override fun launchUrl( + url: String, + preferTor: Boolean, + ) = FavoriteAppLauncher.launchUrl(context, url, preferTor) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt new file mode 100644 index 0000000000..b31a64e385 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/platform/AndroidAppServices.kt @@ -0,0 +1,52 @@ +/* + * 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.platform + +import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.browser.BrowserHistoryRegistry +import com.vitorpamplona.amethyst.commons.browser.BrowserIconRegistry +import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionStore +import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry +import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger +import com.vitorpamplona.amethyst.commons.service.AppServices +import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow +import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver + +/** + * [AppServices] over the main process's app modules. Every member is a getter, so installing + * this object costs nothing and is safe in the `:napplet` process, where `Amethyst.instance` is + * never built, as long as nothing there reads it. + */ +object AndroidAppServices : AppServices { + override val favoriteApps: FavoriteAppsRegistry get() = Amethyst.instance.favoriteApps + + override val browserHistory: BrowserHistoryRegistry get() = Amethyst.instance.browserHistory + + override val browserIcons: BrowserIconRegistry get() = Amethyst.instance.browserIcons + + override val nappletPermissionLedger: NappletPermissionLedger get() = Amethyst.instance.nappletPermissionLedger + + override val signerPermissionStore: NostrSignerPermissionStore get() = Amethyst.instance.signerPermissionStore + + override val torSettings: TorSettingsFlow get() = Amethyst.instance.torPrefs.value + + override val namecoinResolver: NamecoinNameResolver get() = Amethyst.instance.namecoinResolver +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt index 75fe22c6e7..d680149ee3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt @@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.StickToTopOnPrepend import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.UserCompose +import com.vitorpamplona.amethyst.commons.ui.screen.UserFeedViewModel import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt index d601b4f9a2..ba3b6dfea6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn import android.content.res.Configuration -import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight @@ -48,6 +47,7 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.compose.currentBackStackEntryAsState import com.vitorpamplona.amethyst.commons.model.navigation.Route +import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.layouts.LocalScreenLayout import com.vitorpamplona.amethyst.commons.ui.layouts.NavigationStyle import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness @@ -182,7 +182,7 @@ private fun ModalDrawerShell( gesturesEnabled = drawerGesturesEnabled, drawerContent = { DrawerContent(nav, openSheet, accountViewModel) - BackHandler(enabled = nav.drawerState.isOpen, nav::closeDrawer) + PlatformBackHandler(enabled = nav.drawerState.isOpen, nav::closeDrawer) }, content = { if (showRail) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModelAndroidActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModelAndroidActions.kt index 0fcc03aaa3..d4d9d0c2c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModelAndroidActions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModelAndroidActions.kt @@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.failed_to_save_the_video import com.vitorpamplona.amethyst.commons.resources.video_saved_to_the_gallery import com.vitorpamplona.amethyst.commons.service.isVideoOnline +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewState import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel @@ -65,8 +66,8 @@ suspend fun AccountViewModel.checkVideoIsOnline(videoUrl: String): Boolean = htt suspend fun AccountViewModel.uploadMarmotGroupIcon( uri: Uri, mimeType: String?, - context: Context, -): MarmotGroupIconUpload = MarmotGroupIconUploader(account).upload(uri, mimeType, account.settings.defaultFileServer, context) + uploader: MediaUploader, +): MarmotGroupIconUpload = MarmotGroupIconUploader(account).upload(uri, mimeType, account.settings.defaultFileServer, uploader) fun AccountViewModel.saveMediaToGallery( videoUri: String?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt index a3f5135e7d..3f7a1d7466 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AndroidAccountViewModelHost.kt @@ -28,17 +28,24 @@ import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.commons.service.lnurl.LnurlHttpTransport import com.vitorpamplona.amethyst.commons.service.lnurl.OkHttpLnurlTransport import com.vitorpamplona.amethyst.commons.service.pow.PoWJobFailure +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader import com.vitorpamplona.amethyst.commons.tor.MoneyOpRelayRouting +import com.vitorpamplona.amethyst.commons.tor.TorRelayEvaluation import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModelHost import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.dismissNotificationForEvent +import com.vitorpamplona.amethyst.service.uploads.AndroidMediaUploader import com.vitorpamplona.amethyst.ui.note.payViaIntent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.auth.RelayAuthSnapshot import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes +import kotlinx.collections.immutable.PersistentMap import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @@ -60,6 +67,13 @@ class AndroidAccountViewModelHost( override val moneyOpRelays: MoneyOpRelayRouting get() = modules.torEvaluatorFlow + override val torRelayEvaluation: StateFlow get() = modules.torEvaluatorFlow.flow + + override val relayAuthState: StateFlow> + get() = modules.authCoordinator.receiver.authStateFlow + + override val mediaUploader: MediaUploader by lazy { AndroidMediaUploader(modules.appContext) } + override val savedAccounts: Flow> = LocalPreferences .accountsFlow() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt index 3029725769..557a03c373 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt @@ -26,32 +26,32 @@ import androidx.compose.runtime.key import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.model.navigation.BottomBarEntry import com.vitorpamplona.amethyst.commons.model.navigation.NavBarItem +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.articles.datasource.ArticlesFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges.datasource.BadgesFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.list.datasource.CommunitiesListFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.emojipacks.browse.datasource.BrowseEmojiSetsFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.followPacks.list.datasource.FollowPacksFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.datasource.GeocachesFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.gitRepositories.datasource.GitRepositoriesFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.highlights.datasource.HighlightsFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.livestreams.datasource.LiveStreamsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.datasource.MusicTracksFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.datasource.PodcastEpisodesFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.datasource.PodcastsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls.datasource.PollsFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.products.datasource.ProductsFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.publicChats.datasource.PublicChatsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.softwareapps.datasource.SoftwareAppsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.workouts.datasource.WorkoutsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.articles.datasource.ArticlesFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.datasource.BadgesFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.list.datasource.CommunitiesListFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.list.datasource.FollowPacksFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.datasource.LiveStreamsFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.datasource.LongsFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestsFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.datasource.PicturesFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.products.datasource.ProductsFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.publicChats.datasource.PublicChatsFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.datasource.ShortsFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoFilterAssemblerSubscription diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt index 4517e10772..de8a201fc7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt @@ -96,6 +96,15 @@ import com.vitorpamplona.amethyst.commons.resources.backup_conflict_title import com.vitorpamplona.amethyst.commons.resources.backup_review_changed_by_other_app import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups.BackupConflictReviewScreen +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups.BarSegment +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups.ConflictCounts +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups.SplitBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups.StatusTag +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups.clip +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups.conflictTones +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups.countsOf +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups.eventTypeName import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListDiff diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt index b6b8eb9a21..5c658ad8ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.display -import android.content.Intent import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.getValue @@ -29,7 +28,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalClipboard -import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note @@ -60,6 +58,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable import com.vitorpamplona.amethyst.commons.ui.components.M3ActionDialog import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection +import com.vitorpamplona.amethyst.commons.ui.components.rememberTextSharer import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeEditDraftTo @@ -171,7 +170,7 @@ fun BookmarkGroupItemOptionsMenu( onDismiss = onDismiss, ) { val clipboardManager = LocalClipboard.current - val actContext = LocalContext.current + val sharer = rememberTextSharer() val scope = rememberCoroutineScope() // Bookmark Management section @@ -240,23 +239,7 @@ fun BookmarkGroupItemOptionsMenu( } } M3ActionRow(icon = MaterialSymbols.Share, text = stringRes(Res.string.quick_action_share)) { - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra( - Intent.EXTRA_TEXT, - externalLinkForNote(note), - ) - putExtra( - Intent.EXTRA_TITLE, - quickActionShareBrowserLinkStr, - ) - } - - val shareIntent = - Intent.createChooser(sendIntent, quickActionShareStr) - actContext.startActivity(shareIntent) + sharer.share(externalLinkForNote(note), quickActionShareBrowserLinkStr, quickActionShareStr) onDismiss() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.kt index 939def90c5..719bbfe4eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataScreen.kt @@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.commons.resources.follow_set_edit_list_metadat import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events import com.vitorpamplona.amethyst.commons.resources.optional_picture_url import com.vitorpamplona.amethyst.commons.resources.read_only_user +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectSingleFromGallery import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.CreatingTopBar @@ -62,7 +63,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategoryFirstModifier import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt index 4cd31cfd2f..ca9386f767 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt @@ -39,15 +39,15 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_ import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MetadataStripper import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/WebAppScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/WebAppScreen.kt index 7a50ebb6f3..d2947e9dfc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/WebAppScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/WebAppScreen.kt @@ -24,7 +24,6 @@ import android.Manifest import android.content.pm.PackageManager import android.os.Build import android.widget.Toast -import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi @@ -74,6 +73,7 @@ import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.navigation.favoriteIds import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.browser_unsupported +import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -299,9 +299,9 @@ private fun EmbeddedWebAppTab( } val isFullscreen by controller.isFullscreen - BackHandler(enabled = canGoBack && !isFullscreen) { controller.back() } + PlatformBackHandler(enabled = canGoBack && !isFullscreen) { controller.back() } // A fullscreen video inside the tab: back leaves fullscreen first, as in Chrome. - BackHandler(enabled = isFullscreen) { controller.exitFullscreen() } + PlatformBackHandler(enabled = isFullscreen) { controller.exitFullscreen() } EmbeddedPageUi(controller, chrome.ui, showPageInfo, onPageInfoDismiss = { showPageInfo = false }) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzRelayImportViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzRelayImportViewModel.kt index 0a7f5e0893..524539c2b7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzRelayImportViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzRelayImportViewModel.kt @@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupDeletions import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.RELAY_GROUP_METADATA_KINDS import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthDecision +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.reconnectPoolAfterJoin import com.vitorpamplona.quartz.buzz.notifications.MemberAddedNotificationEvent import com.vitorpamplona.quartz.buzz.stream.SystemMessageEvent import com.vitorpamplona.quartz.buzz.workspace.isBuzzDm diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarReminderSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarReminderSettingsScreen.kt index 1115110ab4..d58c2154af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarReminderSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarReminderSettingsScreen.kt @@ -54,13 +54,13 @@ import com.vitorpamplona.amethyst.commons.resources.settings_section_reminders import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsBlockTile +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsDivider +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsSection +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsSwitchTile import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.service.calendar.CalendarReminderWorker import com.vitorpamplona.amethyst.service.calendar.calendarReminderSettings -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsBlockTile -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsDivider -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSection -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSwitchTile import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventScreen.kt index 450b025c40..8a8c97f679 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventScreen.kt @@ -275,7 +275,7 @@ private fun ImageRow( if (uri == null) return@rememberLauncherForActivityResult val mime = context.contentResolver.getType(uri) scope.launch { - val ok = vm.uploadAndSetImage(uri, mime, context) + val ok = vm.uploadAndSetImage(uri, mime, accountViewModel.host.mediaUploader) if (!ok) { accountViewModel.toastManager.toast( Res.string.calendar_event_image_upload_failed, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventViewModel.kt index b8f4d1fe12..0467a042a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarEventViewModel.kt @@ -20,17 +20,17 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.create -import android.content.Context import android.net.Uri import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip52Calendar.parseIsoDateToUnixSeconds +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip01Core.tags.people.PTag @@ -157,7 +157,7 @@ class NewCalendarEventViewModel : ViewModel() { suspend fun uploadAndSetImage( uri: Uri, mimeType: String?, - context: Context, + uploader: MediaUploader, ): Boolean { if (!::account.isInitialized) return false isUploadingImage.value = true @@ -172,7 +172,7 @@ class NewCalendarEventViewModel : ViewModel() { compressionQuality = CompressorQuality.MEDIUM, server = server, account = account, - context = context, + uploader = uploader, ) val serverResult = (result as? UploadingState.Finished)?.result as? UploadOrchestrator.OrchestratorResult.ServerResult diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt index bce0ec65b3..9915e54662 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt @@ -96,13 +96,13 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.commons.ui.note.LoadUser import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.ui.note.types.CalendarRsvpRow import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size30dp import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.addToPhoneCalendar import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.formatCalendarRange diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnComposer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnComposer.kt index 12bc6d3266..c72f18e79b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnComposer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnComposer.kt @@ -49,6 +49,7 @@ import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestio import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.cordn_composer_hint +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -63,7 +64,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt index fba0c1263d..f5815f5152 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt @@ -103,6 +103,9 @@ import com.vitorpamplona.amethyst.commons.resources.cordn_voice_record import com.vitorpamplona.amethyst.commons.resources.cordn_voice_stop import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlVideo +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.components.EmptyState import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.loadStringRes @@ -116,10 +119,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.model.cordn.CordnMediaService -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MetadataStripper import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessageRecorder import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.note.types.RenderAudioWaveformPlayer @@ -141,6 +142,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.io.File /** * One cordn room. @@ -518,7 +520,7 @@ private fun CordnGroupChat( attaching = true attachError = null try { - sendAttachment(context, accountViewModel, room, uploadState) + sendAttachment(context, accountViewModel.host.mediaUploader, accountViewModel, room, uploadState) // Only on success: a failed upload leaves the dialog up // with what you picked still in it, so retrying is one // tap rather than the picker again. @@ -1006,6 +1008,7 @@ private class CordnAttachmentException( */ private suspend fun sendAttachment( context: Context, + uploader: MediaUploader, accountViewModel: AccountViewModel, room: CordnGroupChatroom, state: ChatFileUploadState, @@ -1039,20 +1042,17 @@ private suspend fun sendAttachment( try { // The media-quality slider. // - // Off the main thread, like the strip and the read below it. + // Off the main thread, like the strip and the read below it: // `sendAttachment` is called from the composition's scope, so it - // inherits Main, and `MediaCompressor.compress` opens with - // `checkNotInMainThread()` — so every image attachment threw - // `OnMainThreadException` before it ever reached the uploader. The - // voice path never hit it because it posts its recording already - // encoded and skips compression entirely. + // inherits Main, and compression blocks. The voice path posts its + // recording already encoded and skips compression entirely. val compressed = withContext(Dispatchers.IO) { item.orchestrator.compressIfNeeded( uri = uri, mimeType = declaredMime, - compressionQuality = MediaCompressor.intToCompressorQuality(state.mediaQualitySlider), - context = context, + compressionQuality = CompressorQuality.fromSlider(state.mediaQualitySlider), + uploader = uploader, ) } val mime = compressed.contentType ?: declaredMime @@ -1095,8 +1095,8 @@ private suspend fun sendAttachment( // copies of a message that is end-to-end encrypted everywhere else — the // same reason the voice path deletes its recording. Both calls no-op on the // user's own file. - item.orchestrator.deleteTempUri(finalUri, uri) - item.orchestrator.deleteTempUri(compressed.uri, uri) + deleteTempFile(finalUri, uri) + if (compressed.uri != finalUri) deleteTempFile(compressed.uri, uri) } } finally { // Always, not just on success: a failure leaves the dialog up to retry from, @@ -1334,3 +1334,18 @@ private suspend fun sendVoiceNote( withContext(Dispatchers.IO) { recording.file.delete() } } } + +/** Deletes a pipeline temp file; a no-op on the user's own file, which is [originalUri]. */ +private fun deleteTempFile( + tempUri: Uri, + originalUri: Uri, +) { + if (tempUri == originalUri) return + val path = tempUri.path ?: return + try { + val file = File(path) + if (file.exists() && !file.delete()) Log.w("CordnGroupChat") { "Could not delete temp file $path" } + } catch (e: Exception) { + Log.w("CordnGroupChat", "Failed to delete temp file $path", e) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageActionSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageActionSheet.kt index 2248fe8e99..ae7da2c626 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageActionSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageActionSheet.kt @@ -93,6 +93,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.elements.ShareOptionsBottomShe import com.vitorpamplona.amethyst.commons.ui.note.elements.noteActionSections import com.vitorpamplona.amethyst.commons.ui.note.elements.observeBookmarksFollowsAndAccount import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.report.ReportNoteDialog +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.OnchainZapSendDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.navigateToReloadMint import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness @@ -106,7 +107,6 @@ import com.vitorpamplona.amethyst.ui.actions.EditPostView import com.vitorpamplona.amethyst.ui.note.ZapAmountChoiceGrid import com.vitorpamplona.amethyst.ui.note.observeZapRailCapability import com.vitorpamplona.amethyst.ui.note.payViaIntentOrManualSplit -import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.OnchainZapSendDialog import com.vitorpamplona.quartz.buzz.stream.StreamMessageV2Event import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip22Comments.CommentEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt index b1da5855ba..5b1590c891 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatScreen.kt @@ -73,9 +73,9 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SearchTopBarAction import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.LocalChatActingIdentities import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.LocalChatDisplayNameResolver import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.LocalChatReactOverride diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatsScreen.kt index 3e53546877..71955dd4e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatsScreen.kt @@ -69,16 +69,16 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.UserDrawerSearchTopBar +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChannelLevel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon as SymbolIcon /** * The dedicated top-level list of the user's joined Bitchat-interoperable location channels — the - * geohash-chat analogue of [com.vitorpamplona.amethyst.ui.screen.loggedIn.publicChats.PublicChatsScreen] + * geohash-chat analogue of [com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.publicChats.PublicChatsScreen] * (NIP-28) and the Relay Groups / Concord home screens. Reached from the drawer's "Feeds" section and * pinnable as a bottom-bar tab. Each row opens the cell's chat; the "+" opens [NewGeohashChatScreen] to * join more (near me / manual / teleport). diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt index eb54b6aac2..7e8c436be2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/NewGeohashChatScreen.kt @@ -67,8 +67,8 @@ import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChannelLevel import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash import com.vitorpamplona.amethyst.commons.icons.symbols.Icon as SymbolIcon diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt index 016a0a22fb..6b7216fa01 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/CreateGroupScreen.kt @@ -59,13 +59,13 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_keypackage_relays_not import com.vitorpamplona.amethyst.commons.resources.marmot_keypackage_relays_not_set_title import com.vitorpamplona.amethyst.commons.resources.marmot_skip_for_now import com.vitorpamplona.amethyst.commons.resources.marmot_use_outbox_relays +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.CreatingTopBar import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.screen.loggedIn.uploadMarmotGroupIcon import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.utils.Log @@ -122,7 +122,7 @@ fun CreateGroupScreen( val iconChange = pickedIcon?.let { media -> MarmotGroupIconChange.Set( - accountViewModel.uploadMarmotGroupIcon(media.uri, media.mimeType, context), + accountViewModel.uploadMarmotGroupIcon(media.uri, media.mimeType, accountViewModel.host.mediaUploader), ) } ?: MarmotGroupIconChange.Keep // Always commit an initial metadata extension so that diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt index 9143123543..40e271da12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/EditGroupInfoScreen.kt @@ -57,13 +57,13 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_group_name import com.vitorpamplona.amethyst.commons.resources.marmot_group_name_placeholder import com.vitorpamplona.amethyst.commons.resources.marmot_legacy_group_no_avatar_url import com.vitorpamplona.amethyst.commons.resources.save +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ActionTopBar import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.screen.loggedIn.uploadMarmotGroupIcon import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.coroutines.Dispatchers @@ -114,7 +114,7 @@ fun EditGroupInfoScreen( val iconChange = pickedIcon?.let { media -> MarmotGroupIconChange.Set( - accountViewModel.uploadMarmotGroupIcon(media.uri, media.mimeType, context), + accountViewModel.uploadMarmotGroupIcon(media.uri, media.mimeType, accountViewModel.host.mediaUploader), ) } ?: if (removeIcon) MarmotGroupIconChange.Clear else MarmotGroupIconChange.Keep accountViewModel.updateMarmotGroupMetadata( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt index 405682ed35..b902700071 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt @@ -67,6 +67,8 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_out_of_sync_confirm_b import com.vitorpamplona.amethyst.commons.resources.marmot_out_of_sync_confirm_title import com.vitorpamplona.amethyst.commons.resources.marmot_out_of_sync_reset import com.vitorpamplona.amethyst.commons.resources.reply_here +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -81,8 +83,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightCh import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.LocalChatIsOneOnOne import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView @@ -389,6 +389,7 @@ private fun MarmotGroupFileUploadDialog( } }, context = context, + uploader = accountViewModel.host.mediaUploader, onceUploaded = { uploads -> MarmotFileSender(nostrGroupId, accountViewModel).send(uploads) onUpload() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt index 2a7cbd75ae..bb320241ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupIconEditor.kt @@ -44,11 +44,11 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_add_photo import com.vitorpamplona.amethyst.commons.resources.marmot_change_photo import com.vitorpamplona.amethyst.commons.resources.marmot_group_icon import com.vitorpamplona.amethyst.commons.resources.marmot_remove_photo +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.quartz.nip01Core.core.HexKey /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt index 3ac1b82a62..004bfa0a64 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotFileUploader.kt @@ -25,10 +25,12 @@ import android.net.Uri import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.ui.loadStringRes -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.quartz.marmot.appComponents.EncryptedMediaPolicyV2 import com.vitorpamplona.quartz.marmot.appComponents.EncryptedMediaReferenceV2 @@ -78,6 +80,7 @@ class MarmotFileUploader( exporterSecret: ByteArray, onError: (title: String, message: String) -> Unit, context: Context, + uploader: MediaUploader, /** * Produce `encrypted-media-v2` references instead of MIP-04 ones. * Decided by the group's policy component, not by the uploader. @@ -88,7 +91,7 @@ class MarmotFileUploader( viewState.mediaUploadTracker.startUpload(multiOrchestrator.hasNonMedia()) val results = mutableListOf() - val quality = MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider) + val quality = CompressorQuality.fromSlider(viewState.mediaQualitySlider) val count = multiOrchestrator.size() for (i in 0 until count) { @@ -130,13 +133,13 @@ class MarmotFileUploader( encrypt = cipher, server = viewState.selectedServer, account = account, - context = context, + uploader = uploader, stripMetadata = viewState.stripMetadata, ) val state = item.orchestrator.progressState.value - if (state is UploadingState.Finished && state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - val serverResult = state.result + val serverResult = (state as? UploadingState.Finished)?.result as? UploadOrchestrator.OrchestratorResult.ServerResult + if (serverResult != null) { // The reference is built from what the cipher recorded while // encrypting the bytes the pipeline actually uploaded — after // compression and metadata stripping — because that is what the diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotGroupIconUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotGroupIconUploader.kt index c96b1a3cf2..39004e420c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotGroupIconUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotGroupIconUploader.kt @@ -20,16 +20,16 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send -import android.content.Context import android.net.Uri import com.vitorpamplona.amethyst.commons.marmot.MarmotGroupIconUpload import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.ui.loadStringRes -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupImageCipher import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupImageEncryption import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair @@ -53,13 +53,13 @@ class MarmotGroupIconUploader( uri: Uri, mimeType: String?, server: ServerName, - context: Context, + uploader: MediaUploader, ): MarmotGroupIconUpload { // Compress/downscale up front — avatars don't need full resolution, and a smaller // blob is cheaper for every member to fetch. The MIP-01 crypto uses no AAD and does // not bind the MIME type, so the (possibly transcoded) output type is irrelevant to // decryption; we just hand the compressed bytes to the encrypting uploader. - val compressed = MediaCompressor().compress(uri, mimeType, CompressorQuality.MEDIUM, context.applicationContext) + val compressed = UploadOrchestrator().compressIfNeeded(uri, mimeType, CompressorQuality.MEDIUM, uploader) val uploadMime = compressed.contentType ?: mimeType ?: DEFAULT_MIME val cipher = MarmotGroupImageCipher.forNewImage() val uploadKeySeed = MarmotGroupImageEncryption.generateUploadKey() @@ -76,13 +76,13 @@ class MarmotGroupIconUploader( encrypt = cipher, server = server, account = account, - context = context, + uploader = uploader, stripMetadata = true, forcedSigner = uploadSigner, ) - if (state is UploadingState.Finished && state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - val serverResult = state.result + val serverResult = (state as? UploadingState.Finished)?.result as? UploadOrchestrator.OrchestratorResult.ServerResult + if (serverResult != null) { val hash = serverResult.uploadedHash ?: throw IllegalStateException("Blossom server did not return a content hash for the group icon") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotNewMessageViewModel.kt index 0d5e197d51..edc31b853a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/send/MarmotNewMessageViewModel.kt @@ -33,11 +33,11 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.latestMarmotEdit import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt index a35ffee77e..f54529cd63 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/minichat/MinichatScreen.kt @@ -67,6 +67,8 @@ import com.vitorpamplona.amethyst.commons.resources.chat_minichat_title import com.vitorpamplona.amethyst.commons.resources.chat_send_image_title import com.vitorpamplona.amethyst.commons.resources.concord_open_channel import com.vitorpamplona.amethyst.commons.resources.reply_here +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelSubscription @@ -76,8 +78,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldModifier import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.LocalSuppressReplyToNoteId @@ -240,7 +240,7 @@ fun MinichatScreen( onEncryptedUploadError = { title, message -> launch(Dispatchers.Main) { Toast.makeText(context, "$title: $message", Toast.LENGTH_LONG).show() } }, - context = context, + uploader = accountViewModel.host.mediaUploader, onceUploaded = { uploads -> val caption = uploadState.caption val imetas = if (isConcord) uploads.mapNotNull { it.toConcordImeta() } else uploads.toPlainImetas() 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 5eee33b981..5059cfe763 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 @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send -import android.content.Context import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd @@ -48,6 +47,8 @@ import com.vitorpamplona.amethyst.commons.model.location.LocationResult import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.IZapField @@ -57,7 +58,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.location.LocationState -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileSender import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploader @@ -473,7 +473,7 @@ class ChatNewMessageViewModel : fun uploadAndHold( onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, onceUploaded: () -> Unit, ) { val uploadState = uploadState ?: return @@ -486,8 +486,11 @@ class ChatNewMessageViewModel : encryptedUploadErrorTitle = title encryptedUploadErrorMessage = message pendingRetryMode = RetryMode.HOLD + pendingRetryOnError = onError + pendingRetryUploader = uploader + pendingRetryOnceUploaded = onceUploaded }, - context, + uploader, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) { uploadsWaitingToBeSent += it @@ -499,7 +502,7 @@ class ChatNewMessageViewModel : fun uploadAndSend( onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, onceUploaded: () -> Unit, ) { val room = room.value ?: return @@ -514,10 +517,10 @@ class ChatNewMessageViewModel : encryptedUploadErrorMessage = message pendingRetryMode = RetryMode.SEND pendingRetryOnError = onError - pendingRetryContext = context + pendingRetryUploader = uploader pendingRetryOnceUploaded = onceUploaded }, - context, + uploader, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) { ChatFileSender(room, account).sendNIP17(it) @@ -532,7 +535,7 @@ class ChatNewMessageViewModel : var encryptedUploadErrorMessage by mutableStateOf(null) var pendingRetryMode by mutableStateOf(null) var pendingRetryOnError by mutableStateOf<((String, String) -> Unit)?>(null) - var pendingRetryContext by mutableStateOf(null) + var pendingRetryUploader by mutableStateOf(null) var pendingRetryOnceUploaded by mutableStateOf<(() -> Unit)?>(null) enum class RetryMode { HOLD, SEND } @@ -542,21 +545,23 @@ class ChatNewMessageViewModel : encryptedUploadErrorMessage = null pendingRetryMode = null pendingRetryOnError = null - pendingRetryContext = null + pendingRetryUploader = null pendingRetryOnceUploaded = null } fun retryWithoutEncryption() { val mode = pendingRetryMode ?: return val onError = pendingRetryOnError - val context = pendingRetryContext + val uploader = pendingRetryUploader val onceUploaded = pendingRetryOnceUploaded val room = room.value val uploadState = uploadState dismissEncryptedUploadError() - if (room == null || uploadState == null || context == null) return + // HOLD only uploads; SEND also needs the room to send into. + if (uploadState == null || uploader == null) return + if (mode == RetryMode.SEND && room == null) return uploadState.encryptFiles = false @@ -566,7 +571,7 @@ class ChatNewMessageViewModel : ChatFileUploader(account).justUploadNIP17Unencrypted( uploadState, onError ?: accountViewModel.toastManager::toast, - context, + uploader, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) { uploadsWaitingToBeSent += it @@ -576,13 +581,14 @@ class ChatNewMessageViewModel : } RetryMode.SEND -> { + val sendRoom = room ?: return@launchSigner ChatFileUploader(account).justUploadNIP17Unencrypted( uploadState, onError ?: accountViewModel.toastManager::toast, - context, + uploader, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) { - ChatFileSender(room, account).sendNIP17(it) + ChatFileSender(sendRoom, account).sendNIP17(it) draftTag.newVersion() onceUploaded?.invoke() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt index 8497a8b240..e04b6ce484 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/IMetaAttachments.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.vitorpamplona.amethyst.commons.service.upload.FileHeader -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.service.uploads.prepare import com.vitorpamplona.quartz.nip92IMeta.IMetaTag import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder 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 f5a15641f7..93d0c369e0 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 @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send import android.net.Uri -import androidx.activity.compose.BackHandler import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.layout.Box @@ -88,6 +87,10 @@ import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery +import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeToMessage @@ -118,9 +121,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPa import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField @@ -181,7 +181,7 @@ fun NewGroupDMScreen( WatchAndLoadMyEmojiList(accountViewModel) - BackHandler { + PlatformBackHandler { accountViewModel.launchSigner { postViewModel.sendDraftSync() onUiThread { postViewModel.cancel() } @@ -236,7 +236,6 @@ fun GroupDMScreenContent( nav: INav, ) { val scrollState = rememberScrollState() - val context = LocalContext.current Column(Modifier.fillMaxSize()) { Row(Modifier.fillMaxWidth().padding(horizontal = Size10dp).weight(1f)) { @@ -308,7 +307,7 @@ fun GroupDMScreenContent( onAdd = { _, server, _, _, _, _, _ -> postViewModel.uploadAndHold( accountViewModel.toastManager::toast, - context, + accountViewModel.host.mediaUploader, onceUploaded = { }, ) accountViewModel.account.settings.changeDefaultFileServer(server) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index ac480c057f..b8f96867f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send -import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -43,7 +42,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -63,6 +61,9 @@ import com.vitorpamplona.amethyst.commons.resources.retry_without_encryption import com.vitorpamplona.amethyst.commons.resources.this_message_will_disappear_in import com.vitorpamplona.amethyst.commons.resources.upload_without_encryption_warning import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery +import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor @@ -86,8 +87,6 @@ import com.vitorpamplona.amethyst.commons.util.showCount import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.RoomChatFileUploadDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote @@ -118,7 +117,7 @@ fun PrivateMessageEditFieldRow( onSendNewMessage: () -> Unit, nav: INav, ) { - BackHandler { + PlatformBackHandler { if (channelScreenModel.message.text.isNotBlank()) { accountViewModel.launchSigner { channelScreenModel.sendDraftSync() @@ -187,7 +186,6 @@ fun PrivateMessageEditFieldRow( if (channelScreenModel.wantsExpirationDate) { Row(Modifier.fillMaxWidth().padding(vertical = 2.dp), horizontalArrangement = Arrangement.Center) { - val context = LocalContext.current Text( stringRes( Res.string.this_message_will_disappear_in, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt index c34f038e26..ee873c8af1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.uploa import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.quartz.nip01Core.tags.references.references import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt index 14cebf331d..97bacf2bca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt @@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -49,7 +48,6 @@ fun RoomChatFileUploadDialog( ) { val roomState = channelScreenModel.room.collectAsStateWithLifecycle() val room = roomState.value ?: return - val context = LocalContext.current ChatFileUploadDialog( state, @@ -67,7 +65,7 @@ fun RoomChatFileUploadDialog( upload = { channelScreenModel.uploadAndSend( onError = accountViewModel.toastManager::toast, - context = context, + uploader = accountViewModel.host.mediaUploader, onceUploaded = onUpload, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt index 98eaf8e91d..65926bac51 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt @@ -20,16 +20,17 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload -import android.content.Context import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_encrypted_media_message import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_encrypted_media_title import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.quartz.utils.ciphers.AESGCM @@ -44,7 +45,7 @@ class ChatFileUploader( viewState: ChatFileUploadState, onError: (title: String, message: String) -> Unit, onEncryptedUploadError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, onStrippingFailed: suspend () -> Boolean = { true }, onceUploaded: suspend (List) -> Unit, ) { @@ -58,11 +59,11 @@ class ChatFileUploader( orchestrator.uploadEncrypted( viewState.caption, viewState.contentWarningReason, - MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider), + CompressorQuality.fromSlider(viewState.mediaQualitySlider), cipher, viewState.selectedServer, account, - context, + uploader, stripMetadata = viewState.stripMetadata, onStrippingFailed = onStrippingFailed, ) @@ -70,8 +71,9 @@ class ChatFileUploader( if (results.allGood) { val list = results.successful.mapNotNull { state -> - if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - SuccessfulUploads(state.result, viewState.caption, viewState.contentWarningReason, cipher) + val uploaded = state.result + if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { + SuccessfulUploads(uploaded, viewState.caption, viewState.contentWarningReason, cipher) } else { null } @@ -97,7 +99,7 @@ class ChatFileUploader( } } } else { - justUploadNIP17Unencrypted(viewState, onError, context, onStrippingFailed, onceUploaded) + justUploadNIP17Unencrypted(viewState, onError, uploader, onStrippingFailed, onceUploaded) } viewState.mediaUploadTracker.finishUpload() @@ -106,7 +108,7 @@ class ChatFileUploader( suspend fun justUploadNIP17Unencrypted( viewState: ChatFileUploadState, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, onStrippingFailed: suspend () -> Boolean = { true }, onceUploaded: suspend (List) -> Unit, ) { @@ -117,10 +119,10 @@ class ChatFileUploader( orchestrator.upload( viewState.caption, viewState.contentWarningReason, - MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider), + CompressorQuality.fromSlider(viewState.mediaQualitySlider), viewState.selectedServer, account, - context, + uploader, stripMetadata = viewState.stripMetadata, onStrippingFailed = onStrippingFailed, ) @@ -128,8 +130,9 @@ class ChatFileUploader( if (results.allGood) { val list = results.successful.mapNotNull { state -> - if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - SuccessfulUploads(state.result, viewState.caption, viewState.contentWarningReason, null) + val uploaded = state.result + if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { + SuccessfulUploads(uploaded, viewState.caption, viewState.contentWarningReason, null) } else { null } @@ -153,7 +156,7 @@ class ChatFileUploader( suspend fun justUploadNIP04( viewState: ChatFileUploadState, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, onStrippingFailed: suspend () -> Boolean = { true }, onceUploaded: suspend (List) -> Unit, ) { @@ -164,10 +167,10 @@ class ChatFileUploader( orchestrator.upload( viewState.caption, viewState.contentWarningReason, - MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider), + CompressorQuality.fromSlider(viewState.mediaQualitySlider), viewState.selectedServer, account, - context, + uploader, stripMetadata = viewState.stripMetadata, onStrippingFailed = onStrippingFailed, ) @@ -175,8 +178,9 @@ class ChatFileUploader( if (results.allGood) { val list = results.successful.mapNotNull { state -> - if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - SuccessfulUploads(state.result, viewState.caption, viewState.contentWarningReason, null) + val uploaded = state.result + if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { + SuccessfulUploads(uploaded, viewState.caption, viewState.contentWarningReason, null) } else { null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/SuccessfulUploads.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/SuccessfulUploads.kt index c88223ab0f..f7693c91c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/SuccessfulUploads.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/SuccessfulUploads.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.quartz.utils.ciphers.AESGCM @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt index f8e2c0031d..3f41b23ed2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt @@ -76,6 +76,8 @@ import com.vitorpamplona.amethyst.commons.resources.concord_typing_many import com.vitorpamplona.amethyst.commons.resources.concord_typing_one import com.vitorpamplona.amethyst.commons.resources.concord_typing_two import com.vitorpamplona.amethyst.commons.resources.reply_here +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.feeds.DmHistoryLoadingCard import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachCursor import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachMarkers @@ -101,8 +103,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightCh import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView @@ -639,7 +639,7 @@ private fun ConcordFileUploadDialog( onEncryptedUploadError = { title, message -> scope.launch(Dispatchers.Main) { Toast.makeText(context, "$title: $message", Toast.LENGTH_LONG).show() } }, - context = context, + uploader = accountViewModel.host.mediaUploader, onceUploaded = { uploads -> val imetas = uploads.mapNotNull { it.toConcordImeta() } if (imetas.isNotEmpty()) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt index ea1c2768ab..aa5cf9c9fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/send/ConcordNewMessageViewModel.kt @@ -34,12 +34,12 @@ import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.ReplyMode -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChannelId import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt index b40634d951..c42a93fa4a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt @@ -61,6 +61,7 @@ import com.vitorpamplona.amethyst.commons.resources.public_chat_relays_explainer import com.vitorpamplona.amethyst.commons.resources.public_chat_relays_title import com.vitorpamplona.amethyst.commons.resources.public_chat_title import com.vitorpamplona.amethyst.commons.resources.read_only_user +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectSingleFromGallery import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -68,6 +69,8 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.CreatingTopBar import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.commons.ui.note.LoadPublicChatChannel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding @@ -77,11 +80,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt index 286b7f2a6a..adac712a2b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt @@ -40,17 +40,17 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_ import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MetadataStripper import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip01Core.tags.events.ETag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamGoalHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamGoalHeader.kt index 615928c14a..ae08a351ba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamGoalHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamGoalHeader.kt @@ -41,13 +41,13 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.ui.components.LoadNote import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZapReaction import com.vitorpamplona.amethyst.commons.ui.note.types.GoalProgressBar import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.quartz.nip75ZapGoals.ZapGoalEvent /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt index fcc4e5b605..324fd1b444 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt @@ -109,17 +109,17 @@ import com.vitorpamplona.amethyst.commons.ui.note.timeAgoShort import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzDmListViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.HiddenDmHeader import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupCardWarmupSubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.warningColor import com.vitorpamplona.amethyst.commons.util.sortedBySnapshot import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzAddPeopleDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzDmListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzImportRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzRelayImportViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzWorkspaceOverflowMenu -import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.HiddenDmHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.buzzTimelinePreviewSummary import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupsOnRelaySubscription import com.vitorpamplona.quartz.buzz.workspace.BUZZ_CHANNEL_TYPE_DM diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt index 55720bc405..e22a0582ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataScreen.kt @@ -60,7 +60,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -104,17 +103,17 @@ import com.vitorpamplona.amethyst.commons.resources.relay_group_relay_no_nip29 import com.vitorpamplona.amethyst.commons.resources.relay_group_section_discovery import com.vitorpamplona.amethyst.commons.resources.relay_group_section_discovery_desc import com.vitorpamplona.amethyst.commons.resources.relay_group_section_permissions +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.CreatingTopBar import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.ui.platform.GeohashLocationPickerDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.LoadRelayGroupChannel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupCardWarmupSubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelectSingle -import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.amethyst.ui.note.creators.location.LocationPreviewMap import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash @@ -211,7 +210,6 @@ private fun RelayGroupMetadataScaffold( // existing group already implies a working relay, so callers pass true. nip29Support: Boolean? = true, ) { - val context = LocalContext.current val scrollState = rememberScrollState() // NIP-29 §Subgroups relay support detection: `"nip29": { "subgroups": true }` in the NIP-11. @@ -236,7 +234,7 @@ private fun RelayGroupMetadataScaffold( val onSubmit: () -> Unit = { viewModel.submit( - context = context, + uploader = accountViewModel.host.mediaUploader, onSuccess = onSuccess, onError = accountViewModel.toastManager::toast, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataViewModel.kt index 17cb7d0c4c..a5ded4051f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMetadataViewModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup -import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -29,29 +28,17 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect -import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.resources.Res -import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed -import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events -import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_upload -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled import com.vitorpamplona.amethyst.commons.resources.read_only_user -import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MetadataStripper -import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader -import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.quartz.buzz.workspace.BUZZ_CHANNEL_TYPE_FORUM import com.vitorpamplona.quartz.buzz.workspace.BUZZ_CHANNEL_TYPE_STREAM import com.vitorpamplona.quartz.buzz.workspace.newBuzzChannelId @@ -61,7 +48,6 @@ import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.utils.RandomInstance import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlin.coroutines.cancellation.CancellationException /** * Backs the create/edit NIP-29 group metadata screens. Holds the full editable metadata @@ -221,7 +207,7 @@ class RelayGroupMetadataViewModel : ViewModel() { } fun submit( - context: Context, + uploader: MediaUploader, onSuccess: () -> Unit, onError: (String, String) -> Unit, ) { @@ -231,7 +217,7 @@ class RelayGroupMetadataViewModel : ViewModel() { try { val local = pickedMedia if (local != null) { - val uploadedUrl = uploadImage(local, context, onError) ?: return@launch + val uploadedUrl = uploadImage(local, uploader, onError) ?: return@launch picture.value = TextFieldValue(uploadedUrl) pickedMedia = null } @@ -311,79 +297,7 @@ class RelayGroupMetadataViewModel : ViewModel() { private suspend fun uploadImage( galleryUri: SelectedMedia, - context: Context, + uploader: MediaUploader, onError: (String, String) -> Unit, - ): String? { - val sourceUri = - try { - if (account.settings.stripLocationOnUpload) { - val result = MetadataStripper.strip(galleryUri.uri, galleryUri.mimeType, context.applicationContext) - if (!result.stripped) { - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.metadata_strip_failed_upload_cancelled), - ) - return null - } - result.uri - } else { - galleryUri.uri - } - } catch (e: AvifMetadataNotVerifiableException) { - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName), - ) - return null - } - val compResult = MediaCompressor().compress(sourceUri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext) - - return try { - val result = - if (account.settings.defaultFileServer.type == ServerType.NIP96) { - Nip96Uploader().upload( - uri = compResult.uri, - contentType = compResult.contentType, - size = compResult.size, - alt = null, - sensitiveContent = null, - serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, - onProgress = {}, - httpAuth = account::createHTTPAuthorization, - context = context, - ) - } else { - BlossomUploader().upload( - uri = compResult.uri, - contentType = compResult.contentType, - size = compResult.size, - alt = null, - sensitiveContent = null, - serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, - httpAuth = account::createBlossomUploadAuth, - context = context, - ) - } - - result.url ?: run { - onError( - loadStringRes(Res.string.failed_to_upload_media_no_details), - loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading), - ) - null - } - } catch (_: SignerExceptions.ReadOnlyException) { - onError( - loadStringRes(Res.string.failed_to_upload_media_no_details), - loadStringRes(Res.string.login_with_a_private_key_to_be_able_to_upload), - ) - null - } catch (e: Exception) { - if (e is CancellationException) throw e - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName) - null - } - } + ): String? = uploadToDefaultServer(galleryUri, account, uploader, onError) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt index 6ae466e4a3..539f57af85 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt @@ -30,7 +30,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -59,7 +58,6 @@ fun ChannelFileUploadDialog( nav: INav, ) { val channel = channelScreenModel.channel ?: return - val context = LocalContext.current ChatFileUploadDialog( state, @@ -133,7 +131,7 @@ fun ChannelFileUploadDialog( upload = { channelScreenModel.upload( onError = accountViewModel.toastManager::toast, - context = context, + uploader = accountViewModel.host.mediaUploader, onceUploaded = onUpload, ) 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 4c61d9a10d..68ae70fdf0 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 @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send -import android.content.Context import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd @@ -59,18 +58,20 @@ import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.richtext.UrlParser import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.ReplyMode import com.vitorpamplona.amethyst.service.location.LocationState -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState @@ -495,11 +496,11 @@ open class ChannelNewMessageViewModel : fun upload( onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, onceUploaded: suspend () -> Unit, ) { try { - uploadUnsafe(onError, context, onceUploaded) + uploadUnsafe(onError, uploader, onceUploaded) } catch (_: SignerExceptions.ReadOnlyException) { viewModelScope.launch { onError( @@ -512,7 +513,7 @@ open class ChannelNewMessageViewModel : fun uploadUnsafe( onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, onceUploaded: suspend () -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { @@ -526,10 +527,10 @@ open class ChannelNewMessageViewModel : myMultiOrchestrator.upload( uploadState.caption, uploadState.contentWarningReason, - MediaCompressor.intToCompressorQuality(uploadState.mediaQualitySlider), + CompressorQuality.fromSlider(uploadState.mediaQualitySlider), uploadState.selectedServer, account, - context, + uploader, stripMetadata = uploadState.stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) @@ -537,16 +538,17 @@ open class ChannelNewMessageViewModel : if (results.allGood) { val urls = results.successful.mapNotNull { upload -> - if (upload.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { - val nip95 = account.createNip95(upload.result.bytes, headerInfo = upload.result.fileHeader, uploadState.caption, uploadState.contentWarningReason) + val uploaded = upload.result + if (uploaded is UploadOrchestrator.OrchestratorResult.NIP95Result) { + val nip95 = account.createNip95(uploaded.bytes, headerInfo = uploaded.fileHeader, uploadState.caption, uploadState.contentWarningReason) nip95attachments = nip95attachments + nip95 val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) } note?.toNostrUri() - } else if (upload.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - iMetaAttachments.add(upload.result, uploadState.caption, uploadState.contentWarningReason) + } else if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { + iMetaAttachments.add(uploaded, uploadState.caption, uploadState.contentWarningReason) - upload.result.url + uploaded.url } else { null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt index 6cb3e598d1..3ad55a0c43 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send -import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -54,6 +53,9 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_editing_banner import com.vitorpamplona.amethyst.commons.resources.cancel import com.vitorpamplona.amethyst.commons.resources.reply_here import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery +import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -66,8 +68,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightCh import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote import com.vitorpamplona.quartz.utils.TimeUtils @@ -82,7 +82,7 @@ fun EditFieldRow( onSendNewMessage: suspend () -> Unit, nav: INav, ) { - BackHandler { + PlatformBackHandler { accountViewModel.launchSigner { channelScreenModel.sendDraftSync() onUiThread { channelScreenModel.cancel() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 0ce5c3e505..664d75f72a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -132,9 +132,11 @@ import com.vitorpamplona.amethyst.commons.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.commons.ui.note.ObserveDraftEvent import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.commons.ui.note.elements.ToggleableTimeAgoText +import com.vitorpamplona.amethyst.commons.ui.platform.rememberConcordImageModel import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.observeUserNameByHex import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.header.RoomNameDisplay +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordLeaveDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.coordinatorDisplayName import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.AccountPictureModifier @@ -153,8 +155,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.marmotGro import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.marmotOtherMembers import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.rememberMarmotGroupAvatarUrl import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.reportWarningContentDescription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.ConcordLeaveDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.rememberConcordImageModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.LoadEphemeralChatChannel import com.vitorpamplona.quartz.buzz.notifications.MemberAddedNotificationEvent import com.vitorpamplona.quartz.cordn.appEncryptedMedia.CordnMediaTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt index 4e269a3929..8a536c9a44 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt @@ -64,6 +64,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding +import com.vitorpamplona.amethyst.commons.util.JavaSerializable import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChatroomHeaderCompose import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent @@ -81,7 +82,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter -import java.io.Serializable @Composable fun ChatroomListFeedView( @@ -302,7 +302,7 @@ private fun relayShortName(relay: NormalizedRelayUrl): String = // stores LazyColumn item keys in a SaveableStateHolder, which on Android // requires Bundle-storable types, so each variant is Serializable and only // holds primitives. -private sealed interface ChatroomLazyKey : Serializable +private sealed interface ChatroomLazyKey : JavaSerializable private data class MarmotChatroomLazyKey( val groupId: HexKey, 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 8b52378ae7..488b3b7ea2 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 @@ -72,6 +72,8 @@ import com.vitorpamplona.amethyst.commons.resources.media_compression_quality_me import com.vitorpamplona.amethyst.commons.resources.media_compression_quality_uncompressed import com.vitorpamplona.amethyst.commons.resources.strip_metadata_description import com.vitorpamplona.amethyst.commons.resources.strip_metadata_label +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.ShowImageUploadGallery +import com.vitorpamplona.amethyst.commons.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe @@ -80,13 +82,11 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TitleIconModifier import com.vitorpamplona.amethyst.commons.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.SettingSwitchItem +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery -import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import kotlinx.collections.immutable.toImmutableList @OptIn(ExperimentalMaterial3Api::class) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt index ea55a4ee76..2703b316e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt @@ -28,9 +28,9 @@ import androidx.compose.runtime.setValue import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMediaProcessing import kotlinx.collections.immutable.ImmutableList @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessRelaySettingsSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessRelaySettingsSheet.kt index 10e0639223..16af1d0b1f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessRelaySettingsSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessRelaySettingsSheet.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chess -import android.annotation.SuppressLint import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -225,7 +224,7 @@ fun ChessRelaySettingsSheet( } } -@SuppressLint("StateFlowValueCalledInComposition") +@Suppress("StateFlowValueCalledInComposition") @Composable fun StatRow(chessViewModel: ChessViewModel) { Column(horizontalAlignment = Alignment.CenterHorizontally) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt index a785dc80d5..ab7d7a646e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt @@ -22,10 +22,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chess import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.nip64Chess.ChessDismissedGamesStore import com.vitorpamplona.amethyst.commons.nip64Chess.ChessPollingDefaults import com.vitorpamplona.amethyst.commons.nip64Chess.ChessViewModel +import kotlin.reflect.KClass /** * Factory for the shared [ChessViewModel], wired with the Android adapters. @@ -34,8 +36,11 @@ class ChessViewModelFactory( private val account: Account, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(ChessViewModel::class.java)) { + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T { + if (modelClass == ChessViewModel::class) { return ChessViewModel( userPubkey = account.userProfile().pubkeyHex, publisher = AndroidChessPublisher(account), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt index c32b75daab..b8727ee1e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt @@ -79,6 +79,7 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.ChannelCardCompose import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt index 9759574d1a..14a038c7db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm -import androidx.activity.compose.BackHandler import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -70,7 +69,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization @@ -102,7 +100,12 @@ import com.vitorpamplona.amethyst.commons.resources.url_slug_placeholder import com.vitorpamplona.amethyst.commons.resources.your_article_title import com.vitorpamplona.amethyst.commons.resources.zapraiser import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage +import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.ContentSensitivityExplainer @@ -116,6 +119,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowU import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraiserButton import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapToButton +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.text.onUiThread @@ -125,10 +129,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPa import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelectSingle -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField @@ -141,7 +141,6 @@ import com.vitorpamplona.amethyst.ui.note.creators.location.GeoHashPostSection import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ObserveInboxRelayListAndDisplayIfNotFound -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.collections.immutable.persistentListOf @@ -169,7 +168,7 @@ fun LongFormPostScreen( StrippingFailureDialog(postViewModel.strippingFailureConfirmation) - BackHandler { + PlatformBackHandler { accountViewModel.launchSigner { postViewModel.sendDraftSync() onUiThread { postViewModel.cancel() } @@ -238,15 +237,13 @@ private fun MarkdownPostScreenBody( .fillMaxWidth() .verticalScroll(scrollState, reverseScrolling = true), ) { - val context = LocalContext.current - // Banner image area BannerImageArea( coverImageUrl = postViewModel.coverImageUrl, isUploading = postViewModel.isUploadingCoverImage, accountViewModel = accountViewModel, onImageSelected = { - postViewModel.uploadCoverImage(it, context, onError = accountViewModel.toastManager::toast) + postViewModel.uploadCoverImage(it, accountViewModel.host.mediaUploader, onError = accountViewModel.toastManager::toast) }, onClear = { postViewModel.coverImageUrl = "" @@ -492,13 +489,12 @@ private fun MarkdownPostScreenBody( verticalAlignment = CenterVertically, modifier = Modifier.padding(vertical = Size10dp, horizontal = Size10dp), ) { - val uploadContext = LocalContext.current ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, useH265, stripMetadata, convertGifToMp4 -> - postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, uploadContext, useH265, stripMetadata, convertGifToMp4) + postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, accountViewModel.host.mediaUploader, useH265, stripMetadata, convertGifToMp4) accountViewModel.account.settings.changeDefaultFileServer(server) }, onDelete = postViewModel::deleteMediaToUpload, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt index 470e776a6e..527a64cf98 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm -import android.content.Context import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd @@ -46,17 +45,21 @@ import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder import com.vitorpamplona.amethyst.commons.model.composer.toZapSplitSetup import com.vitorpamplona.amethyst.commons.model.location.LocationResult import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName -import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState.EmojiMedia import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events import com.vitorpamplona.amethyst.commons.resources.read_only_user -import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMediaProcessing +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState @@ -66,16 +69,10 @@ import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource +import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.location.LocationState -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader -import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor @@ -122,7 +119,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import kotlin.coroutines.cancellation.CancellationException import kotlin.uuid.ExperimentalUuidApi @Stable @@ -467,7 +463,7 @@ class LongFormPostViewModel : fun uploadCoverImage( uri: SelectedMedia, - context: Context, + uploader: MediaUploader, onError: (String, String) -> Unit, ) { accountViewModel.launchSigner { @@ -475,7 +471,7 @@ class LongFormPostViewModel : try { directUpload( galleryUri = uri, - context = context, + uploader = uploader, onError = onError, )?.let { coverImageUrl = it @@ -488,51 +484,11 @@ class LongFormPostViewModel : private suspend fun directUpload( galleryUri: SelectedMedia, - context: Context, + uploader: MediaUploader, onError: (String, String) -> Unit, - ): String? { - val compResult = MediaCompressor().compress(galleryUri.uri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext) - - return try { - val result = - if (account.settings.defaultFileServer.type == ServerType.NIP96) { - Nip96Uploader().upload( - uri = compResult.uri, - contentType = compResult.contentType, - size = compResult.size, - alt = null, - sensitiveContent = null, - serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = accountViewModel.httpClientBuilder::okHttpClientForUploads, - onProgress = {}, - httpAuth = account::createHTTPAuthorization, - context = context, - ) - } else { - BlossomUploader().upload( - uri = compResult.uri, - contentType = compResult.contentType, - size = compResult.size, - alt = null, - sensitiveContent = null, - serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = accountViewModel.httpClientBuilder::okHttpClientForUploads, - httpAuth = account::createBlossomUploadAuth, - context = context, - ) - } - - if (result.url == null) { - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading)) - } - - result.url - } catch (e: Exception) { - if (e is CancellationException) throw e - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName) - null - } - } + ): String? = + // Cover images were never stripped: keep them as picked, only compressed. + uploadToDefaultServer(galleryUri, account, uploader, onError, stripMetadata = false) fun upload( alt: String?, @@ -540,13 +496,13 @@ class LongFormPostViewModel : mediaQuality: Int, server: ServerName, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, useH265: Boolean, stripMetadata: Boolean = true, convertGifToMp4: Boolean = false, ) { try { - uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, context, useH265, stripMetadata, convertGifToMp4) + uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, uploader, useH265, stripMetadata, convertGifToMp4) } catch (_: SignerExceptions.ReadOnlyException) { viewModelScope.launch { onError( @@ -563,7 +519,7 @@ class LongFormPostViewModel : mediaQuality: Int, server: ServerName, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, useH265: Boolean, stripMetadata: Boolean = true, convertGifToMp4: Boolean = false, @@ -577,10 +533,10 @@ class LongFormPostViewModel : myMultiOrchestrator.upload( alt, contentWarningReason, - MediaCompressor.intToCompressorQuality(mediaQuality), + CompressorQuality.fromSlider(mediaQuality), server, account, - context, + uploader, useH265, stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, @@ -590,29 +546,30 @@ class LongFormPostViewModel : if (results.allGood) { val urls = results.successful.mapNotNull { state -> - if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { + val uploaded = state.result + if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { val iMeta = - IMetaTagBuilder(state.result.url) + IMetaTagBuilder(uploaded.url) .apply { - hash(state.result.fileHeader.hash) - size(state.result.fileHeader.size) - state.result.fileHeader.mimeType + hash(uploaded.fileHeader.hash) + size(uploaded.fileHeader.size) + uploaded.fileHeader.mimeType ?.let { mimeType(it) } - state.result.fileHeader.dim + uploaded.fileHeader.dim ?.let { dims(it) } - state.result.fileHeader.blurHash + uploaded.fileHeader.blurHash ?.let { blurhash(it.blurhash) } - state.result.fileHeader.thumbHash + uploaded.fileHeader.thumbHash ?.let { thumbhash(it.thumbhash) } - state.result.magnet?.let { magnet(it) } - state.result.uploadedHash?.let { originalHash(it) } + uploaded.magnet?.let { magnet(it) } + uploaded.uploadedHash?.let { originalHash(it) } alt?.let { alt(it) } contentWarningReason?.let { sensitiveContent(contentWarningReason) } }.build() iMetaAttachments.replace(iMeta.url, iMeta) - val markdownImage = "![${alt ?: ""}](${state.result.url})" + val markdownImage = "![${alt ?: ""}](${uploaded.url})" markdownImage } else { null 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 0d283ddfb9..b06be8a622 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 @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds -import androidx.activity.compose.BackHandler import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -53,6 +52,10 @@ import com.vitorpamplona.amethyst.commons.resources.lightning_invoice import com.vitorpamplona.amethyst.commons.resources.new_product import com.vitorpamplona.amethyst.commons.resources.zapraiser import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery +import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar @@ -75,9 +78,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.navigation.navs.Nav @@ -148,7 +148,7 @@ fun NewProductScreen( StrippingFailureDialog(postViewModel.strippingFailureConfirmation) - BackHandler { + PlatformBackHandler { accountViewModel.launchSigner { postViewModel.sendDraftSync() onUiThread { postViewModel.cancel() } @@ -292,7 +292,7 @@ private fun NewProductBody( defaultServer = accountViewModel.account.settings.defaultFileServer, isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, _, stripMetadata, _ -> - postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context, stripMetadata) + postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, accountViewModel.host.mediaUploader, stripMetadata) accountViewModel.account.settings.changeDefaultFileServer(server) }, onDelete = postViewModel::deleteMediaToUpload, 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 c73f4fbe0a..a5b3dfdad5 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 @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds -import android.content.Context import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd @@ -52,6 +51,12 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMediaProcessing +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState @@ -60,13 +65,9 @@ import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.location.LocationState -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor @@ -400,7 +401,7 @@ open class NewProductViewModel : mediaQuality: Int, server: ServerName, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, stripMetadata: Boolean = true, ) { viewModelScope.launch(Dispatchers.IO) { @@ -413,40 +414,41 @@ open class NewProductViewModel : myMultiOrchestrator.upload( alt, contentWarningReason, - MediaCompressor.intToCompressorQuality(mediaQuality), + CompressorQuality.fromSlider(mediaQuality), server, myAccount, - context, + uploader, stripMetadata = stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) if (results.allGood) { val urls = - results.successful.mapNotNull { - if (it.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - if (it.result.fileHeader.mimeType + results.successful.mapNotNull { finished -> + val uploaded = finished.result + if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { + if (uploaded.fileHeader.mimeType ?.startsWith("image") == true ) { productImages = productImages + ProductImageMeta( - url = it.result.url, - mimeType = it.result.fileHeader.mimeType, + url = uploaded.url, + mimeType = uploaded.fileHeader.mimeType, blurhash = - it.result.fileHeader.blurHash + uploaded.fileHeader.blurHash ?.blurhash, - dimension = it.result.fileHeader.dim, + dimension = uploaded.fileHeader.dim, alt = alt, - hash = it.result.fileHeader.hash, - size = it.result.fileHeader.size, + hash = uploaded.fileHeader.hash, + size = uploaded.fileHeader.size, thumbhash = - it.result.fileHeader.thumbHash + uploaded.fileHeader.thumbHash ?.thumbhash, ) } else { - iMetaDescription.add(it.result, alt, contentWarningReason) + iMetaDescription.add(uploaded, alt, contentWarningReason) - it.result.url + uploaded.url } } else { null 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 dae7daa668..11bc0c490b 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 @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds -import android.annotation.SuppressLint import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -97,7 +96,7 @@ import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.quartz.nip99Classifieds.tags.ConditionTag import kotlinx.collections.immutable.toImmutableList -@SuppressLint("ViewModelConstructorInComposable") +@Suppress("ViewModelConstructorInComposable") @Preview @Composable fun SellProductPreview() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabLayer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabLayer.kt index ba49a8480c..e63004a330 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabLayer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabLayer.kt @@ -29,7 +29,6 @@ import android.graphics.BitmapFactory import android.os.Build import android.os.SystemClock import android.view.ViewGroup -import androidx.activity.compose.BackHandler import androidx.annotation.RequiresApi import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Canvas @@ -90,6 +89,7 @@ import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPillEvent import com.vitorpamplona.amethyst.commons.browser.ui.pill.ConsoleLine import com.vitorpamplona.amethyst.commons.browser.ui.pill.ConsoleSheet import com.vitorpamplona.amethyst.commons.browser.ui.pill.FindInPagePill +import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.napplethost.BrowserWebTools import kotlinx.coroutines.delay import kotlinx.serialization.json.buildJsonObject @@ -306,7 +306,7 @@ fun EmbeddedTabLayer(barFavoriteIds: List) { } if (pillExpanded) { - BackHandler { pillExpanded = false } + PlatformBackHandler { pillExpanded = false } Box( Modifier .fillMaxSize() @@ -361,7 +361,7 @@ fun EmbeddedTabLayer(barFavoriteIds: List) { // Find in page: opened from the pill's Find tile. if (findShowing && findBridge != null) { - BackHandler { closeFind() } + PlatformBackHandler { closeFind() } val result by findBridge.findResult FindInPagePill( query = findQuery, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/RemoteImeView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/RemoteImeView.kt index aed8de884d..4f746b18bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/RemoteImeView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/RemoteImeView.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.embed -import android.annotation.SuppressLint import android.content.Context import android.os.SystemClock import android.text.Editable @@ -52,7 +51,7 @@ import kotlinx.serialization.json.put * It covers the soft keyboard, hardware keyboards, autofill, paste, and context-menu edits uniformly, * because every one of them mutates the same [Editable] and we flush the resulting state. */ -@SuppressLint("ViewConstructor", "AppCompatCustomView") +@Suppress("ViewConstructor", "AppCompatCustomView") class RemoteImeView( context: Context, ) : EditText(context) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackViewModel.kt deleted file mode 100644 index 25c362c7bf..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackViewModel.kt +++ /dev/null @@ -1,192 +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.emojipacks.display - -import android.content.Context -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.commons.model.Account -import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType -import com.vitorpamplona.amethyst.commons.resources.Res -import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed -import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details -import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_upload -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled -import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading -import com.vitorpamplona.amethyst.commons.ui.loadStringRes -import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MetadataStripper -import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader -import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions -import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import kotlin.coroutines.cancellation.CancellationException - -@Stable -class EmojiPackViewModel( - val account: Account, - val packIdentifier: String, -) : ViewModel() { - val selectedPackFlow = - account.ownedEmojiPacks - .getOwnedEmojiPackFlow(packIdentifier) - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(2500), null) - - var isUploadingEmojiImage by mutableStateOf(false) - - suspend fun addEmoji( - emoji: EmojiUrlTag, - isPrivate: Boolean, - ) { - account.addEmojiToOwnedPack(packIdentifier, emoji, isPrivate) - } - - suspend fun removeEmoji( - shortcode: String, - isPrivate: Boolean, - ) { - account.removeEmojiFromOwnedPack(packIdentifier, shortcode, isPrivate) - } - - suspend fun deletePack() { - account.deleteOwnedEmojiPack(packIdentifier) - } - - fun uploadEmojiImage( - uri: SelectedMedia, - context: Context, - onUploaded: (String) -> Unit, - onError: (String, String) -> Unit, - ) { - viewModelScope.launch(Dispatchers.IO) { - upload( - uri, - context, - onUploading = { isUploadingEmojiImage = it }, - onUploaded = onUploaded, - onError = onError, - ) - } - } - - private suspend fun upload( - galleryUri: SelectedMedia, - context: Context, - onUploading: (Boolean) -> Unit, - onUploaded: (String) -> Unit, - onError: (String, String) -> Unit, - ) { - onUploading(true) - - val sourceUri = - try { - if (account.settings.stripLocationOnUpload) { - val result = MetadataStripper.strip(galleryUri.uri, galleryUri.mimeType, context.applicationContext) - if (!result.stripped) { - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.metadata_strip_failed_upload_cancelled), - ) - onUploading(false) - return - } - result.uri - } else { - galleryUri.uri - } - } catch (e: AvifMetadataNotVerifiableException) { - onUploading(false) - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName), - ) - return - } - val compResult = MediaCompressor().compress(sourceUri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext) - - try { - val result = - if (account.settings.defaultFileServer.type == ServerType.NIP96) { - Nip96Uploader().upload( - uri = compResult.uri, - contentType = compResult.contentType, - size = compResult.size, - alt = null, - sensitiveContent = null, - serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, - onProgress = {}, - httpAuth = account::createHTTPAuthorization, - context = context, - ) - } else { - BlossomUploader().upload( - uri = compResult.uri, - contentType = compResult.contentType, - size = compResult.size, - alt = null, - sensitiveContent = null, - serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, - httpAuth = account::createBlossomUploadAuth, - context = context, - ) - } - - if (result.url != null) { - onUploading(false) - onUploaded(result.url) - } else { - onUploading(false) - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading)) - } - } catch (_: SignerExceptions.ReadOnlyException) { - onUploading(false) - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.login_with_a_private_key_to_be_able_to_upload)) - } catch (e: Exception) { - if (e is CancellationException) throw e - onUploading(false) - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName) - } - } - - @Suppress("UNCHECKED_CAST") - class Initializer( - val account: Account, - val packIdentifier: String, - ) : ViewModelProvider.NewInstanceFactory() { - override fun create(modelClass: Class): T = EmojiPackViewModel(account, packIdentifier) as T - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/NostrAppScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/NostrAppScreen.kt index 5dccbbf0dc..41306f42d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/NostrAppScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/NostrAppScreen.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.favorites import android.os.Build import android.widget.Toast -import androidx.activity.compose.BackHandler import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -73,6 +72,7 @@ import com.vitorpamplona.amethyst.commons.resources.favorite_apps import com.vitorpamplona.amethyst.commons.resources.favorite_notice_paid import com.vitorpamplona.amethyst.commons.resources.favorite_notice_published import com.vitorpamplona.amethyst.commons.resources.favorite_notice_uploaded +import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar @@ -262,7 +262,7 @@ private fun EmbeddedNostrAppTab( onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } - BackHandler(enabled = canGoBack) { controller.back() } + PlatformBackHandler(enabled = canGoBack) { controller.back() } if (showAccess) { Dialog(onDismissRequest = { showAccess = false }, properties = DialogProperties(usePlatformDefaultWidth = false)) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt index bc55ce3516..5f500691c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed -import android.annotation.SuppressLint import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -69,6 +68,8 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TitleIconModifier import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarSize import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote +import com.vitorpamplona.amethyst.commons.ui.note.platform.LikeReaction +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZapReaction import com.vitorpamplona.amethyst.commons.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.HalfHorzSpacer @@ -76,9 +77,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size18Modifier import com.vitorpamplona.amethyst.commons.ui.theme.SpacedBy2dp import com.vitorpamplona.amethyst.commons.ui.theme.TabRowHeight import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.ReplyReaction -import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.screen.UserFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal.FollowPackFeedConversationsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal.FollowPackFeedNewThreadFeedViewModel @@ -106,7 +105,7 @@ fun FollowPackFeedScreen( } } -@SuppressLint("StateFlowValueCalledInComposition") +@Suppress("StateFlowValueCalledInComposition") @Composable fun PrepareViewModelsFollowPackScreen( note: AddressableNote, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersUserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersUserFeedViewModel.kt index 83b6356ff3..391d510789 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersUserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersUserFeedViewModel.kt @@ -24,7 +24,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.UserFeedViewModel class FollowPackMembersUserFeedViewModel( val followPackNote: AddressableNote, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt index 45f447610e..d99e892f54 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheScreen.kt @@ -67,7 +67,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -106,10 +105,10 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.geocacheEmoji import com.vitorpamplona.amethyst.commons.ui.note.geocacheLabelRes import com.vitorpamplona.amethyst.commons.ui.platform.GeohashLocationPickerDialog +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.note.creators.location.LocationPreviewMap -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.quartz.nip01Core.tags.geohash.toGeoHash import com.vitorpamplona.quartz.nipCCGeocaching.listing.GeocacheGeohash import com.vitorpamplona.quartz.nipCCGeocaching.listing.tags.CacheSize @@ -142,7 +141,6 @@ fun NewGeocacheScreen( ) { val model: NewGeocacheViewModel = viewModel() val scope = rememberCoroutineScope() - val context = LocalContext.current remember(editKind, editPubKeyHex, editDTag, prefillGeohash) { model.init(accountViewModel) @@ -158,7 +156,7 @@ fun NewGeocacheScreen( val photoPicker = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> - if (uri != null) scope.launch { model.uploadImage(uri, null, context) } + if (uri != null) scope.launch { model.uploadImage(uri, null, accountViewModel.host.mediaUploader) } } Scaffold( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheViewModel.kt index e1dfe1e218..1c60602098 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/create/NewGeocacheViewModel.kt @@ -20,17 +20,17 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.create -import android.content.Context import android.net.Uri import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey @@ -177,7 +177,7 @@ class NewGeocacheViewModel : ViewModel() { suspend fun uploadImage( uri: Uri, mimeType: String?, - context: Context, + uploader: MediaUploader, ): Boolean { if (!::account.isInitialized) return false isUploading.value = true @@ -191,7 +191,7 @@ class NewGeocacheViewModel : ViewModel() { compressionQuality = CompressorQuality.MEDIUM, server = account.settings.defaultFileServer, account = account, - context = context, + uploader = uploader, ) val url = ((result as? UploadingState.Finished)?.result as? UploadOrchestrator.OrchestratorResult.ServerResult)?.url diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheOwnerActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheOwnerActions.kt index 11d10a89a2..f69a4d5c89 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheOwnerActions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheOwnerActions.kt @@ -60,9 +60,9 @@ import com.vitorpamplona.amethyst.commons.resources.geocache_owner_no_finders import com.vitorpamplona.amethyst.commons.resources.geocache_owner_qr_warning import com.vitorpamplona.amethyst.commons.resources.geocache_owner_show_qr import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nipCCGeocaching.firstToFind.FirstToFindResolver import com.vitorpamplona.quartz.nipCCGeocaching.listing.GeocacheListingEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt index 4422ea12d1..c5f9554506 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt @@ -27,8 +27,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel import com.vitorpamplona.amethyst.ui.note.nip22Comments.GenericCommentPostScreen diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryHomeUi.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryHomeUi.kt index fdb89b6ed8..7952419dc3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryHomeUi.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryHomeUi.kt @@ -70,11 +70,11 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo +import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip34Git.git.GitCommit import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/code/GitCodeTab.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/code/GitCodeTab.kt index 7926d67b00..04a9464841 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/code/GitCodeTab.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/code/GitCodeTab.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.code -import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll @@ -73,6 +72,7 @@ import com.vitorpamplona.amethyst.commons.resources.git_repo_no_clone_url import com.vitorpamplona.amethyst.commons.resources.git_repo_no_search_results import com.vitorpamplona.amethyst.commons.resources.git_repo_root import com.vitorpamplona.amethyst.commons.resources.git_repo_search_files +import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.layouts.LocalDisappearingBarState import com.vitorpamplona.amethyst.commons.ui.layouts.LocalDisappearingScaffoldPadding import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -141,7 +141,7 @@ private fun CodeBrowser( if (openPath != null) { val entry = remember(openFilePath) { snapshot.entryAt(openPath) } - BackHandler { openFilePath = null } + PlatformBackHandler { openFilePath = null } Column(Modifier.fillMaxSize().padding(scaffoldPaddingTop)) { FileHeader(name = openPath.lastOrNull() ?: "", onBack = { openFilePath = null }) HorizontalDivider(thickness = 0.5.dp) @@ -164,7 +164,7 @@ private fun CodeBrowser( var query by rememberSaveable(snapshot.headCommit) { mutableStateOf("") } val searching = query.isNotBlank() - BackHandler(enabled = searching || path.isNotEmpty()) { + PlatformBackHandler(enabled = searching || path.isNotEmpty()) { if (searching) query = "" else pathString = path.dropLast(1).joinToString("/") } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/code/GitCommitLog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/code/GitCommitLog.kt index 22b7e59ed2..295232a901 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/code/GitCommitLog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/code/GitCommitLog.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.code import android.text.format.DateUtils -import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -66,6 +65,7 @@ import com.vitorpamplona.amethyst.commons.resources.git_repo_code_loading import com.vitorpamplona.amethyst.commons.resources.git_repo_commits import com.vitorpamplona.amethyst.commons.resources.git_repo_file_load_error import com.vitorpamplona.amethyst.commons.resources.git_repo_no_commits +import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.commons.ui.note.GitDiffView import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -103,7 +103,7 @@ fun GitCommitLog( if (openCommit != null) { val commit = remember(openCommit, commits) { commits?.firstOrNull { it.oid == openCommit } } - BackHandler { openCommit = null } + PlatformBackHandler { openCommit = null } Column(Modifier.fillMaxSize()) { LogHeader(title = commit?.shortOid ?: "", onBack = { openCommit = null }) HorizontalDivider(thickness = 0.5.dp) @@ -116,7 +116,7 @@ fun GitCommitLog( return } - BackHandler { onBack() } + PlatformBackHandler { onBack() } Column(Modifier.fillMaxSize()) { LogHeader(title = stringRes(Res.string.git_repo_commits), onBack = onBack) HorizontalDivider(thickness = 0.5.dp) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt index d43a6bc4ca..f82ffcca8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt @@ -27,8 +27,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel import com.vitorpamplona.amethyst.ui.note.nip22Comments.GenericCommentPostScreen diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayListDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayListDialog.kt index 374ab4452a..34932d3318 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayListDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AddOutboxRelayListDialog.kt @@ -40,13 +40,13 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.outbox_relays_not_found_editing import com.vitorpamplona.amethyst.commons.resources.outbox_relays_not_found_examples import com.vitorpamplona.amethyst.commons.resources.outbox_relays_title +import com.vitorpamplona.amethyst.commons.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.Nip65OutboxRelayList import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.Nip65RelayListViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AlgoFeedStatusBanner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AlgoFeedStatusBanner.kt index 3d2372b282..5ba2457880 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AlgoFeedStatusBanner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/AlgoFeedStatusBanner.kt @@ -54,12 +54,12 @@ import com.vitorpamplona.amethyst.commons.resources.dvm_offline_banner import com.vitorpamplona.amethyst.commons.ui.components.LoadNote import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.dvms.DvmPaymentActions import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.dvms.rememberDvmHeartbeatFresh import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.DvmPaymentActions import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt index bc4a6b369b..8ff65412ac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt @@ -92,6 +92,8 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.zonedDrawerSwipeIfModal import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geohash.NewGeoPostButton +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag.NewHashtagPostButton import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding @@ -102,8 +104,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.TabRowHeight import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.backups.BackupConflictCards -import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.NewGeoPostButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.NewHashtagPostButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.live.RenderEphemeralBubble import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.live.RenderGeohashBubble 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 5823df6c8b..de50091951 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 @@ -20,10 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home -import android.annotation.SuppressLint import android.content.Intent import android.net.Uri -import androidx.activity.compose.BackHandler import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable @@ -111,6 +109,10 @@ import com.vitorpamplona.amethyst.commons.resources.schedule_post_always_on_prom import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind import com.vitorpamplona.amethyst.commons.resources.zapraiser import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery +import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.BaseUserPicture @@ -129,6 +131,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraise import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapToButton import com.vitorpamplona.amethyst.commons.ui.note.types.ReplyRenderType +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness @@ -146,9 +149,6 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS import com.vitorpamplona.amethyst.ui.actions.uploads.RecordVoiceButton -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator @@ -173,7 +173,6 @@ import com.vitorpamplona.amethyst.ui.note.creators.scheduling.roundUpToNextQuart import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.ui.note.creators.zappolls.ZapPollField import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -276,7 +275,7 @@ internal fun NewPostScreenInner( StrippingFailureDialog(postViewModel.strippingFailureConfirmation) - BackHandler { + PlatformBackHandler { accountViewModel.launchSigner { postViewModel.sendDraftSync() onUiThread { postViewModel.cancel() } @@ -617,7 +616,7 @@ private fun NewPostScreenBody( accountViewModel.account.settings.defaultFileServer, isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, useH265, stripMetadata, convertGifToMp4 -> - postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context, useH265, stripMetadata, convertGifToMp4) + postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, accountViewModel.host.mediaUploader, useH265, stripMetadata, convertGifToMp4) accountViewModel.account.settings.changeDefaultFileServer(server) }, onDelete = postViewModel::deleteMediaToUpload, @@ -953,7 +952,7 @@ private fun BottomRowActions( } } -@SuppressLint("ViewModelConstructorInComposable") +@Suppress("ViewModelConstructorInComposable") @Preview @Composable private fun BottomRowActionsPreview() { 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 3ddf7c7f8a..629fb3cc2d 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 @@ -69,6 +69,13 @@ import com.vitorpamplona.amethyst.commons.service.ai.WritingTone import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMediaProcessing +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.IAudience @@ -80,18 +87,12 @@ import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.text.setTextAndPlaceCursorAtBeginning +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState import com.vitorpamplona.amethyst.service.ai.WritingAssistantFactory import com.vitorpamplona.amethyst.service.location.LocationState -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationController import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments @@ -1578,13 +1579,13 @@ open class ShortNotePostViewModel : mediaQuality: Int, server: ServerName, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, useH265: Boolean, stripMetadata: Boolean = true, convertGifToMp4: Boolean = false, ) { try { - uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, context, useH265, stripMetadata, convertGifToMp4) + uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, uploader, useH265, stripMetadata, convertGifToMp4) } catch (_: SignerExceptions.ReadOnlyException) { viewModelScope.launch { onError( @@ -1601,7 +1602,7 @@ open class ShortNotePostViewModel : mediaQuality: Int, server: ServerName, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, useH265: Boolean, stripMetadata: Boolean = true, convertGifToMp4: Boolean = false, @@ -1615,10 +1616,10 @@ open class ShortNotePostViewModel : myMultiOrchestrator.upload( alt, contentWarningReason, - MediaCompressor.intToCompressorQuality(mediaQuality), + CompressorQuality.fromSlider(mediaQuality), server, account, - context, + uploader, useH265, stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, @@ -1629,30 +1630,31 @@ open class ShortNotePostViewModel : if (results.allGood) { val urls = results.successful.mapNotNull { state -> - if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { - val nip95 = account.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason) + val uploaded = state.result + if (uploaded is UploadOrchestrator.OrchestratorResult.NIP95Result) { + val nip95 = account.createNip95(uploaded.bytes, headerInfo = uploaded.fileHeader, alt, contentWarningReason) nip95attachments = nip95attachments + nip95 val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) } note?.let { "nostr:" + it.toNEvent() } - } else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { + } else if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { val iMeta = - IMetaTagBuilder(state.result.url) + IMetaTagBuilder(uploaded.url) .apply { - hash(state.result.fileHeader.hash) - size(state.result.fileHeader.size) - state.result.fileHeader.mimeType + hash(uploaded.fileHeader.hash) + size(uploaded.fileHeader.size) + uploaded.fileHeader.mimeType ?.let { mimeType(it) } - state.result.fileHeader.dim + uploaded.fileHeader.dim ?.let { dims(it) } - state.result.fileHeader.blurHash + uploaded.fileHeader.blurHash ?.let { blurhash(it.blurhash) } - state.result.fileHeader.thumbHash + uploaded.fileHeader.thumbHash ?.let { thumbhash(it.thumbhash) } - state.result.magnet?.let { magnet(it) } - state.result.uploadedHash?.let { originalHash(it) } + uploaded.magnet?.let { magnet(it) } + uploaded.uploadedHash?.let { originalHash(it) } alt?.let { alt(it) } contentWarningReason?.let { sensitiveContent(contentWarningReason) } @@ -1660,7 +1662,7 @@ open class ShortNotePostViewModel : iMetaAttachments.replace(iMeta.url, iMeta) - state.result.url + uploaded.url } else { null } @@ -1935,7 +1937,6 @@ open class ShortNotePostViewModel : val recording = voiceRecording ?: return val fileToUpload = activeFile ?: recording.file val waveform = activeWaveform ?: recording.amplitudes - val appContext = Amethyst.instance.appContext val uploadErrorTitle = loadStringRes(Res.string.upload_error_title) val uploadVoiceNip95NotSupported = loadStringRes(Res.string.upload_error_voice_message_nip95_not_supported) val uploadVoiceFailed = loadStringRes(Res.string.upload_error_voice_message_failed) @@ -1956,7 +1957,7 @@ open class ShortNotePostViewModel : compressionQuality = CompressorQuality.UNCOMPRESSED, server = server, account = account, - context = appContext, + uploader = accountViewModel.host.mediaUploader, useH265 = false, forcedSigner = if (wantsAnonymousPost) anonymousSigner() else null, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt index 6eb9f05f9d..f9b5232f26 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home -import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets @@ -40,6 +39,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose import com.vitorpamplona.amethyst.commons.ui.note.types.ReplyRenderType @@ -71,7 +71,7 @@ fun VoiceReplyScreen( viewModel.load(replyToNoteId, recordingFilePath, mimeType, duration, amplitudesJson) } - BackHandler { + PlatformBackHandler { viewModel.cancel() nav.popBack() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index 0dfff06e30..850b450647 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -38,12 +38,12 @@ import com.vitorpamplona.amethyst.commons.resources.upload_error_voice_message_e import com.vitorpamplona.amethyst.commons.resources.upload_error_voice_message_failed import com.vitorpamplona.amethyst.commons.resources.upload_error_voice_message_nip95_not_supported import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.util.deleteOrWarn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationController import com.vitorpamplona.quartz.nip01Core.tags.people.toPTag @@ -204,7 +204,7 @@ class VoiceReplyViewModel : ViewModel() { compressionQuality = CompressorQuality.UNCOMPRESSED, server = serverToUse, account = accountViewModel.account, - context = Amethyst.instance.appContext, + uploader = accountViewModel.host.mediaUploader, useH265 = false, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderGeohashBubble.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderGeohashBubble.kt index 1d3aa3d9fa..430fa01877 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderGeohashBubble.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderGeohashBubble.kt @@ -34,9 +34,9 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.amethyst.commons.icons.symbols.Icon as SymbolIcon /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/nip75Goals/NewGoalScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/nip75Goals/NewGoalScreen.kt index a8fc08fab7..fe49ecb469 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/nip75Goals/NewGoalScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/nip75Goals/NewGoalScreen.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.nip75Goals -import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -59,6 +58,7 @@ import com.vitorpamplona.amethyst.commons.resources.goal_summary_placeholder import com.vitorpamplona.amethyst.commons.resources.goal_website_label import com.vitorpamplona.amethyst.commons.resources.goal_website_placeholder import com.vitorpamplona.amethyst.commons.resources.new_goal +import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar @@ -93,7 +93,7 @@ fun NewGoalScreen( accountViewModel: AccountViewModel, nav: INav, ) { - BackHandler { + PlatformBackHandler { goalViewModel.cancel() nav.popBack() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/AccountBackupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/AccountBackupScreen.kt index 804ce0e0ef..cee705213a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/AccountBackupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/AccountBackupScreen.kt @@ -136,14 +136,14 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.BackButton +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.note.authenticate import com.vitorpamplona.amethyst.ui.note.rememberAuthPromptLabels -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.quartz.nip19Bech32.toNsec import com.vitorpamplona.quartz.nip49PrivKeyEnc.Nip49 import kotlinx.coroutines.CoroutineScope diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataScreen.kt index 82bf221d7f..79579f1835 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataScreen.kt @@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.commons.resources.follow_pack_title import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events import com.vitorpamplona.amethyst.commons.resources.picture_url import com.vitorpamplona.amethyst.commons.resources.read_only_user +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectSingleFromGallery import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -66,7 +67,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt index 9429e26f05..5b66e7921c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt @@ -39,15 +39,15 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_ import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MetadataStripper import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataScreen.kt index 34d71f6a98..4d2a4b3ecb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataScreen.kt @@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.commons.resources.people_list_explainer import com.vitorpamplona.amethyst.commons.resources.people_list_title import com.vitorpamplona.amethyst.commons.resources.picture_url import com.vitorpamplona.amethyst.commons.resources.read_only_user +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectSingleFromGallery import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -66,7 +67,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt index f015379575..d3d833908c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt @@ -39,15 +39,15 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_ import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MetadataStripper import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/NewLongVideoButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/NewLongVideoButton.kt index 23f91913bc..72ac47929c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/NewLongVideoButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/NewLongVideoButton.kt @@ -50,6 +50,8 @@ import com.vitorpamplona.amethyst.commons.resources.ic_compose import com.vitorpamplona.amethyst.commons.resources.new_long_video import com.vitorpamplona.amethyst.commons.resources.record_a_video import com.vitorpamplona.amethyst.commons.resources.upload_image +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -58,8 +60,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.NewMediaModel import com.vitorpamplona.amethyst.ui.actions.NewMediaView -import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelect -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideo import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt index 4c130e5156..647b2bebbb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt @@ -84,16 +84,19 @@ import com.vitorpamplona.amethyst.commons.resources.music_track_title_placeholde import com.vitorpamplona.amethyst.commons.resources.music_track_uploading_banner import com.vitorpamplona.amethyst.commons.resources.new_music_track import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SendingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.CoverImagePicker +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.UploadInProgressBanner +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.UploadPlaceholder import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelectSingle -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import kotlinx.collections.immutable.persistentListOf @OptIn(ExperimentalMaterial3Api::class) @@ -154,7 +157,6 @@ fun NewMusicTrackScreen( onPost = { if (!vm.isValid() || isBusy) return@SendingTopBar vm.saveAndPublish( - context = context, accountViewModel = accountViewModel, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt index 602f125695..b4332daaea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt @@ -30,13 +30,16 @@ import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState +import com.vitorpamplona.amethyst.commons.service.uploads.lastPathSegmentOrNull import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent import com.vitorpamplona.quartz.nip01Core.core.Address import kotlinx.collections.immutable.ImmutableList @@ -174,7 +177,7 @@ class NewMusicTrackViewModel : ViewModel() { audioMedia.value = MultiOrchestrator(persistentListOf(uri)) // Fall back to the last path segment of the content:// URI — Android picker URIs // rarely expose a clean filename, but the segment is at least stable across recomposes. - pickedAudioName.value = uri.uri.lastPathSegment?.substringAfterLast('/') + pickedAudioName.value = uri.uri.lastPathSegmentOrNull()?.substringAfterLast('/') // Auto-fill title/artist/album/duration from the picked file's embedded metadata so // the user doesn't have to retype what's already in the ID3 / mp4 / FLAC tags. @@ -257,10 +260,7 @@ class NewMusicTrackViewModel : ViewModel() { * Errors are reported through the global toast manager rather than a callback because * the screen may already be gone by the time we know the outcome. */ - fun saveAndPublish( - context: Context, - accountViewModel: AccountViewModel, - ) { + fun saveAndPublish(accountViewModel: AccountViewModel) { if (isSending.value) return // double-tap guard // Snapshot every input the upload needs into immutable locals BEFORE launching, so @@ -288,10 +288,10 @@ class NewMusicTrackViewModel : ViewModel() { existingCoverUrl = coverUrl.value.trim().ifBlank { null }, existingAudioUrl = audioUrl.value.trim(), server = server, - quality = MediaCompressor.intToCompressorQuality(mediaQualitySlider.value), + quality = CompressorQuality.fromSlider(mediaQualitySlider.value), stripMetadata = stripMetadata.value, loadedEvent = loadedEvent, - appContext = context.applicationContext, + uploader = accountViewModel.host.mediaUploader, ) isSending.value = true @@ -362,7 +362,7 @@ class NewMusicTrackViewModel : ViewModel() { val quality: CompressorQuality, val stripMetadata: Boolean, val loadedEvent: MusicTrackEvent?, - val appContext: Context, + val uploader: MediaUploader, ) /** @@ -403,13 +403,13 @@ class NewMusicTrackViewModel : ViewModel() { mediaQuality = snapshot.quality, server = snapshot.server, account = account, - context = snapshot.appContext, + uploader = snapshot.uploader, useH265 = false, stripMetadata = snapshot.stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) if (!res.allGood) { - throw UploadException(kind, formatUploadErrors(res.errors, snapshot.appContext)) + throw UploadException(kind, formatUploadErrors(res.errors)) } return firstUploadedUrl(res.successful) ?: throw UploadException(kind, "Server didn't return a URL for the uploaded $kind.") @@ -420,15 +420,12 @@ class NewMusicTrackViewModel : ViewModel() { details: String, ) : Exception("$kind upload failed: $details") - private fun firstUploadedUrl(successful: List): String? = + private fun firstUploadedUrl(successful: List): String? = successful .firstNotNullOfOrNull { it.result as? UploadOrchestrator.OrchestratorResult.ServerResult } ?.url - private suspend fun formatUploadErrors( - errors: List, - context: Context, - ): String = + private suspend fun formatUploadErrors(errors: List): String = errors .map { loadStringRes(it.errorResource, *it.params) } .distinct() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt index 6a6b38d0c7..cf52314cdb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt @@ -84,7 +84,15 @@ import com.vitorpamplona.amethyst.commons.napplet.ui.PolicyCard import com.vitorpamplona.amethyst.commons.napplet.ui.symbol import com.vitorpamplona.amethyst.commons.nip46RemoteSigner.ui.Nip46ActivityCard import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.browser_permission_allowed +import com.vitorpamplona.amethyst.commons.resources.browser_permission_ask +import com.vitorpamplona.amethyst.commons.resources.browser_permission_blocked +import com.vitorpamplona.amethyst.commons.resources.browser_permission_camera +import com.vitorpamplona.amethyst.commons.resources.browser_permission_location +import com.vitorpamplona.amethyst.commons.resources.browser_permission_microphone +import com.vitorpamplona.amethyst.commons.resources.browser_permission_section import com.vitorpamplona.amethyst.commons.resources.cancel +import com.vitorpamplona.amethyst.commons.resources.confirm import com.vitorpamplona.amethyst.commons.resources.napplet_connected_app_capabilities import com.vitorpamplona.amethyst.commons.resources.napplet_connected_app_forget import com.vitorpamplona.amethyst.commons.resources.napplet_connected_app_op_overrides @@ -106,6 +114,7 @@ import com.vitorpamplona.amethyst.commons.resources.napplet_policy_paranoid_desc import com.vitorpamplona.amethyst.commons.resources.napplet_policy_reasonable import com.vitorpamplona.amethyst.commons.resources.napplet_policy_reasonable_desc import com.vitorpamplona.amethyst.commons.resources.napplet_signer_permissions_revoke_all +import com.vitorpamplona.amethyst.commons.resources.napplet_untitled import com.vitorpamplona.amethyst.commons.resources.nip46_signer_activity_title import com.vitorpamplona.amethyst.commons.resources.nip46_signer_app_relays_inbox import com.vitorpamplona.amethyst.commons.resources.nip46_signer_app_relays_own_hint @@ -114,9 +123,10 @@ import com.vitorpamplona.amethyst.commons.resources.nip46_signer_reconnecting import com.vitorpamplona.amethyst.commons.resources.nip46_signer_remote_app import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.platform.rememberManifestIconModel +import com.vitorpamplona.amethyst.commons.ui.platform.rememberWebAppIconModel +import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.favorites.rememberManifestIconModel -import com.vitorpamplona.amethyst.favorites.rememberWebAppIconModel import com.vitorpamplona.amethyst.napplet.NappletBrokerService import com.vitorpamplona.amethyst.napplet.WebSitePermissionRegistry import com.vitorpamplona.amethyst.napplet.counterpartyLabel @@ -129,13 +139,12 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46Reconne import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46StatusDot import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.nip46AppOnline import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.nip46ClientSubtitle -import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import com.vitorpamplona.amethyst.commons.R as CommonsR +import org.jetbrains.compose.resources.StringResource private data class ConnectedAppDetailState( val title: String, @@ -154,7 +163,7 @@ fun ConnectedAppDetailScreen( ) { val capabilityLedger = Amethyst.instance.nappletPermissionLedger val signerLedger = remember { NostrSignerPermissionLedger(Amethyst.instance.signerPermissionStore) } - val untitled = stringRes(CommonsR.string.napplet_untitled) + val untitled = stringRes(Res.string.napplet_untitled) var state by remember { mutableStateOf(null) } var reload by remember { mutableIntStateOf(0) } @@ -557,7 +566,7 @@ private const val BROWSER_PREFIX = "browser:" private fun SitePermissionsSection(origin: String) { val all by WebSitePermissionRegistry.decisions.collectAsStateWithLifecycle() val decisions = all[origin].orEmpty() - SectionHeader(stringRes(CommonsR.string.browser_permission_section)) + SectionHeader(stringRes(Res.string.browser_permission_section)) Surface( color = MaterialTheme.colorScheme.surfaceVariant, shape = MaterialTheme.shapes.medium, @@ -575,9 +584,9 @@ private fun SitePermissionsSection(origin: String) { Text( stringRes( when (permission) { - BrowserSitePermission.CAMERA -> CommonsR.string.browser_permission_camera - BrowserSitePermission.MICROPHONE -> CommonsR.string.browser_permission_microphone - BrowserSitePermission.LOCATION -> CommonsR.string.browser_permission_location + BrowserSitePermission.CAMERA -> Res.string.browser_permission_camera + BrowserSitePermission.MICROPHONE -> Res.string.browser_permission_microphone + BrowserSitePermission.LOCATION -> Res.string.browser_permission_location }, ), style = MaterialTheme.typography.bodyMedium, @@ -603,11 +612,11 @@ private fun SitePermissionsSection(origin: String) { } } -private fun BrowserSitePermission.Decision.labelRes(): Int = +private fun BrowserSitePermission.Decision.labelRes(): StringResource = when (this) { - BrowserSitePermission.Decision.ASK -> CommonsR.string.browser_permission_ask - BrowserSitePermission.Decision.ALLOW -> CommonsR.string.browser_permission_allowed - BrowserSitePermission.Decision.BLOCK -> CommonsR.string.browser_permission_blocked + BrowserSitePermission.Decision.ASK -> Res.string.browser_permission_ask + BrowserSitePermission.Decision.ALLOW -> Res.string.browser_permission_allowed + BrowserSitePermission.Decision.BLOCK -> Res.string.browser_permission_blocked } @Composable @@ -800,7 +809,7 @@ private fun CapabilityPermissionDialog( TextButton( onClick = { onSetGrant(if (selected == GrantState.ASK) null else selected) }, ) { - Text(stringRes(android.R.string.ok)) + Text(stringRes(Res.string.confirm)) } }, dismissButton = { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/NappletsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/NappletsScreen.kt index f28fc919f6..42cf90faf5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/NappletsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/NappletsScreen.kt @@ -43,10 +43,10 @@ import com.vitorpamplona.amethyst.commons.resources.napplet_none_found import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.napplets.datasource.NappletsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.napplet.NappletLauncher -import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource.NappletsFilterAssemblerSubscription import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip5dNapplets.NamedNappletEvent import com.vitorpamplona.quartz.nip5dNapplets.RootNappletEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt index 86d189d6b6..68b0c4b973 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt @@ -91,7 +91,10 @@ import com.vitorpamplona.amethyst.commons.ui.note.LongPressToQuickAction import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.commons.ui.note.elements.MoreOptionsButton +import com.vitorpamplona.amethyst.commons.ui.note.platform.LikeReaction +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZapReaction import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip53LiveActivities.LoadParticipants import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer @@ -105,9 +108,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.StdPadding import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.LikeReaction -import com.vitorpamplona.amethyst.ui.note.ZapReaction -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.LoadParticipants import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestActivity import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/create/CreateNestSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/create/CreateNestSheet.kt index c062d737cc..2ea4775057 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/create/CreateNestSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/create/CreateNestSheet.kt @@ -70,10 +70,10 @@ import com.vitorpamplona.amethyst.commons.resources.nest_create_submit import com.vitorpamplona.amethyst.commons.resources.nest_create_title import com.vitorpamplona.amethyst.commons.resources.nest_create_when import com.vitorpamplona.amethyst.commons.resources.next +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectSingleFromGallery import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.NestsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestActivity import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/create/CreateNestViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/create/CreateNestViewModel.kt index 314f882a8a..e72eba5d8d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/create/CreateNestViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/create/CreateNestViewModel.kt @@ -31,15 +31,15 @@ import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_de import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MetadataStripper import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity.NestActivity import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.endpoint diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/activity/NestActivityContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/activity/NestActivityContent.kt index 959c747bce..b0e5bed777 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/activity/NestActivityContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/activity/NestActivityContent.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.activity -import androidx.activity.compose.BackHandler import androidx.activity.compose.LocalActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -52,6 +51,7 @@ import com.vitorpamplona.amethyst.commons.resources.nest_loading_room import com.vitorpamplona.amethyst.commons.resources.nest_unjoinable_back import com.vitorpamplona.amethyst.commons.resources.nest_unjoinable_body import com.vitorpamplona.amethyst.commons.resources.nest_unjoinable_title +import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel @@ -362,7 +362,7 @@ private fun NestActivityBody( // Failed) or PIP isn't supported, we let back fall through to the // default Activity.finish() so the user can cancel out cleanly. val canMinimize = isPipSupported && !isInPipMode && ui.connection is ConnectionUiState.Connected - BackHandler(enabled = canMinimize) { onMinimize() } + PlatformBackHandler(enabled = canMinimize) { onMinimize() } if (isInPipMode) { NestPipScreen( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt index 11b20b6327..ac621ee4df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt @@ -40,6 +40,8 @@ import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggesti import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.reply_here import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -51,8 +53,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightCh import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote import kotlinx.collections.immutable.persistentListOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestFileUploadDialog.kt index 8c8c0e6afc..111e99080d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestFileUploadDialog.kt @@ -29,7 +29,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -56,7 +55,6 @@ fun NestFileUploadDialog( nav: INav, ) { val room = nestScreenModel.room ?: return - val context = LocalContext.current val host = room.author ?: return val title = (room.event as? MeetingSpaceEvent)?.room().orEmpty() @@ -93,7 +91,7 @@ fun NestFileUploadDialog( upload = { nestScreenModel.upload( onError = accountViewModel.toastManager::toast, - context = context, + uploader = accountViewModel.host.mediaUploader, onceUploaded = onUpload, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt index cb1dd4ce5c..378d7a8aa3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.room.chat -import android.content.Context import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd @@ -50,17 +49,19 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_ import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.richtext.UrlParser import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.location.LocationState -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState @@ -349,11 +350,11 @@ open class NestNewMessageViewModel : fun upload( onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, onceUploaded: suspend () -> Unit, ) { try { - uploadUnsafe(onError, context, onceUploaded) + uploadUnsafe(onError, uploader, onceUploaded) } catch (_: SignerExceptions.ReadOnlyException) { viewModelScope.launch { onError( @@ -366,7 +367,7 @@ open class NestNewMessageViewModel : fun uploadUnsafe( onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, onceUploaded: suspend () -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { @@ -380,10 +381,10 @@ open class NestNewMessageViewModel : myMultiOrchestrator.upload( uploadState.caption, uploadState.contentWarningReason, - MediaCompressor.intToCompressorQuality(uploadState.mediaQualitySlider), + CompressorQuality.fromSlider(uploadState.mediaQualitySlider), uploadState.selectedServer, account, - context, + uploader, stripMetadata = uploadState.stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) @@ -391,16 +392,17 @@ open class NestNewMessageViewModel : if (results.allGood) { val urls = results.successful.mapNotNull { upload -> - if (upload.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { - val nip95 = account.createNip95(upload.result.bytes, headerInfo = upload.result.fileHeader, uploadState.caption, uploadState.contentWarningReason) + val uploaded = upload.result + if (uploaded is UploadOrchestrator.OrchestratorResult.NIP95Result) { + val nip95 = account.createNip95(uploaded.bytes, headerInfo = uploaded.fileHeader, uploadState.caption, uploadState.contentWarningReason) nip95attachments = nip95attachments + nip95 val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) } note?.toNostrUri() - } else if (upload.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - iMetaAttachments.add(upload.result, uploadState.caption, uploadState.contentWarningReason) + } else if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { + iMetaAttachments.add(uploaded, uploadState.caption, uploadState.contentWarningReason) - upload.result.url + uploaded.url } else { null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/AddInboxRelayListDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/AddInboxRelayListDialog.kt index 95b78161eb..5b6ac5f244 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/AddInboxRelayListDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/AddInboxRelayListDialog.kt @@ -40,13 +40,13 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.inbox_relays_not_found_editing import com.vitorpamplona.amethyst.commons.resources.inbox_relays_not_found_examples import com.vitorpamplona.amethyst.commons.resources.inbox_relays_title +import com.vitorpamplona.amethyst.commons.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.Nip65InboxRelayList import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.Nip65RelayListViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt index b0484b9cce..980a70bba4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/donations/ZapTheDevsCard.kt @@ -67,12 +67,14 @@ import com.vitorpamplona.amethyst.commons.resources.version_name import com.vitorpamplona.amethyst.commons.resources.zap_the_devs_description import com.vitorpamplona.amethyst.commons.resources.zap_the_devs_title import com.vitorpamplona.amethyst.commons.ui.components.LoadNote +import com.vitorpamplona.amethyst.commons.ui.components.ZapButtonConfig import com.vitorpamplona.amethyst.commons.ui.components.appendLink import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.commons.ui.note.ZapIcon import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.DisplayZapSplits +import com.vitorpamplona.amethyst.commons.ui.note.platform.ReusableZapButton import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp @@ -80,8 +82,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.components.ReusableZapButton -import com.vitorpamplona.amethyst.ui.components.ZapButtonConfig import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking @@ -267,10 +267,7 @@ fun ZapDonationButton( accountViewModel: AccountViewModel, nav: INav, ) { - val config = - ZapButtonConfig( - grayTint = grayTint, - ) + val config = remember(grayTint) { ZapButtonConfig(grayTint = grayTint) } ReusableZapButton( baseNote = baseNote, 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 724e63e4c6..b3b9bce7ae 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 @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages -import androidx.activity.compose.BackHandler import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.layout.Column @@ -48,7 +47,6 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -61,6 +59,10 @@ import com.vitorpamplona.amethyst.commons.resources.messages_new_message_to_capt import com.vitorpamplona.amethyst.commons.resources.public_message import com.vitorpamplona.amethyst.commons.resources.zapraiser import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery +import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar @@ -89,9 +91,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField @@ -142,7 +141,7 @@ fun NewPublicMessageScreen( StrippingFailureDialog(postViewModel.strippingFailureConfirmation) - BackHandler { + PlatformBackHandler { accountViewModel.launchSigner { postViewModel.sendDraftSync() onUiThread { postViewModel.cancel() } @@ -260,13 +259,12 @@ fun PublicMessageScreenContent( verticalAlignment = CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp), ) { - val context = LocalContext.current ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, _, stripMetadata, _ -> - postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context, stripMetadata) + postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, accountViewModel.host.mediaUploader, stripMetadata) accountViewModel.account.settings.changeDefaultFileServer(server) }, onDelete = postViewModel::deleteMediaToUpload, 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 b28456d3db..4031174597 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 @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages -import android.content.Context import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd @@ -54,6 +53,12 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_ import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMediaProcessing +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState @@ -62,13 +67,9 @@ import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.location.LocationState -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor @@ -430,11 +431,11 @@ class NewPublicMessageViewModel : mediaQuality: Int, server: ServerName, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, stripMetadata: Boolean = true, ) { try { - uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, context, stripMetadata) + uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, uploader, stripMetadata) } catch (e: SignerExceptions.ReadOnlyException) { viewModelScope.launch { onError( @@ -451,7 +452,7 @@ class NewPublicMessageViewModel : mediaQuality: Int, server: ServerName, onError: (title: String, message: String) -> Unit, - context: Context, + uploader: MediaUploader, stripMetadata: Boolean = true, ) { viewModelScope.launch(Dispatchers.IO) { @@ -463,10 +464,10 @@ class NewPublicMessageViewModel : myMultiOrchestrator.upload( alt, contentWarningReason, - MediaCompressor.intToCompressorQuality(mediaQuality), + CompressorQuality.fromSlider(mediaQuality), server, account, - context, + uploader, stripMetadata = stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) @@ -474,30 +475,31 @@ class NewPublicMessageViewModel : if (results.allGood) { val urls = results.successful.mapNotNull { state -> - if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { - val nip95 = account.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason) + val uploaded = state.result + if (uploaded is UploadOrchestrator.OrchestratorResult.NIP95Result) { + val nip95 = account.createNip95(uploaded.bytes, headerInfo = uploaded.fileHeader, alt, contentWarningReason) nip95attachments = nip95attachments + nip95 val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) } note?.let { "nostr:" + it.toNEvent() } - } else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { + } else if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { val iMeta = - IMetaTagBuilder(state.result.url) + IMetaTagBuilder(uploaded.url) .apply { - hash(state.result.fileHeader.hash) - size(state.result.fileHeader.size) - state.result.fileHeader.mimeType + hash(uploaded.fileHeader.hash) + size(uploaded.fileHeader.size) + uploaded.fileHeader.mimeType ?.let { mimeType(it) } - state.result.fileHeader.dim + uploaded.fileHeader.dim ?.let { dims(it) } - state.result.fileHeader.blurHash + uploaded.fileHeader.blurHash ?.let { blurhash(it.blurhash) } - state.result.fileHeader.thumbHash + uploaded.fileHeader.thumbHash ?.let { thumbhash(it.thumbhash) } - state.result.magnet?.let { magnet(it) } - state.result.uploadedHash?.let { originalHash(it) } + uploaded.magnet?.let { magnet(it) } + uploaded.uploadedHash?.let { originalHash(it) } alt?.let { alt(it) } contentWarningReason?.let { sensitiveContent(contentWarningReason) } @@ -505,7 +507,7 @@ class NewPublicMessageViewModel : iMetaAttachments.replace(iMeta.url, iMeta) - state.result.url + uploaded.url } else { null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt index 29e9486023..bf80d12fbb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt @@ -51,6 +51,8 @@ import com.vitorpamplona.amethyst.commons.resources.ic_compose import com.vitorpamplona.amethyst.commons.resources.new_picture import com.vitorpamplona.amethyst.commons.resources.take_a_picture import com.vitorpamplona.amethyst.commons.resources.upload_image +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -59,8 +61,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.NewMediaModel import com.vitorpamplona.amethyst.ui.actions.NewMediaView -import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelect -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.TakePicture import com.vitorpamplona.amethyst.ui.actions.uploads.resolveSharedMedia import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt index ccd21a2374..cd3a8ed3d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt @@ -49,9 +49,9 @@ import com.vitorpamplona.amethyst.commons.ui.components.ContentWarningGate import com.vitorpamplona.amethyst.commons.ui.components.collectContentWarningReasons import com.vitorpamplona.amethyst.commons.ui.components.mediaSizingModifier import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.components.ZoomableContentView -import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.UserCardHeader import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip68Picture.PictureEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt index 3d74d3d04d..f6a1dd63f6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt @@ -44,13 +44,13 @@ import com.vitorpamplona.amethyst.commons.resources.podcast_season import com.vitorpamplona.amethyst.commons.resources.podcast_season_episode import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor +import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeAgoLabels import com.vitorpamplona.amethyst.commons.ui.note.timeAgoWith import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.note.types.PodcastEpisodeAudioPlayer import com.vitorpamplona.quartz.podcasts.PodcastEpisode diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastHeader.kt index 1d92d618ce..16432a19d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastHeader.kt @@ -44,6 +44,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.podcast_episode_count import com.vitorpamplona.amethyst.commons.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.ui.note.types.PodcastCoverCard import com.vitorpamplona.amethyst.commons.ui.note.types.PodcastPeople import com.vitorpamplona.amethyst.commons.ui.pluralStringRes @@ -51,7 +52,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.quartz.nipF4Podcasts.metadata.PodcastMetadataEvent import com.vitorpamplona.quartz.podcasts.PodcastShow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeScreen.kt index c21e4a2857..d8126aaeff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeScreen.kt @@ -90,16 +90,17 @@ import com.vitorpamplona.amethyst.commons.resources.podcast_episode_video_label import com.vitorpamplona.amethyst.commons.resources.podcast_new_episode import com.vitorpamplona.amethyst.commons.resources.podcast_publishing_banner import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SendingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.CoverImagePicker +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.UploadInProgressBanner +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.UploadPlaceholder +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.V4VSplitEditor import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelectSingle -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.CoverImagePicker -import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.UploadInProgressBanner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.UploadPlaceholder import kotlinx.collections.immutable.persistentListOf import org.jetbrains.compose.resources.StringResource @@ -152,7 +153,7 @@ fun NewPodcastEpisodeScreen( isActive = { vm.isValid() && !isBusy }, onPost = { if (!vm.isValid() || isBusy) return@SendingTopBar - vm.saveAndPublish(context, accountViewModel) + vm.saveAndPublish(accountViewModel) }, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeViewModel.kt index a43c37fa88..5579d02896 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeViewModel.kt @@ -29,10 +29,13 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.podcasts.V4VSplitEditorState import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.lastPathSegmentOrNull +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.PodcastComposerMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nipXXPodcasting20.episode.Podcasting20EpisodeEvent import com.vitorpamplona.quartz.podcasts.PodcastAudio @@ -153,11 +156,11 @@ class NewPodcastEpisodeViewModel : ViewModel() { return } audioMedia.value = MultiOrchestrator(persistentListOf(uri)) - pickedAudioName.value = uri.uri.lastPathSegment?.substringAfterLast('/') + pickedAudioName.value = uri.uri.lastPathSegmentOrNull()?.substringAfterLast('/') val appContext = context.applicationContext viewModelScope.launch(Dispatchers.IO) { - val probed = PodcastComposerMedia.probeAudio(appContext, uri.uri) ?: return@launch + val probed = PodcastAudioProbe.probeAudio(appContext, uri.uri) ?: return@launch withContext(Dispatchers.Main.immediate) { probed.durationSeconds?.let { durationSeconds.value = it.toString() } if (title.value.isBlank()) probed.title?.let { title.value = it } @@ -173,10 +176,7 @@ class NewPodcastEpisodeViewModel : ViewModel() { /** Valid with a title and a resolvable audio source (picked file or a pasted URL). */ fun isValid(): Boolean = title.value.isNotBlank() && (audioMedia.value != null || audioUrl.value.isNotBlank()) - fun saveAndPublish( - context: Context, - accountViewModel: AccountViewModel, - ) { + fun saveAndPublish(accountViewModel: AccountViewModel) { if (isSending.value) return val server = selectedServer.value @@ -205,9 +205,9 @@ class NewPodcastEpisodeViewModel : ViewModel() { existingCoverUrl = coverUrl.value.trim().ifBlank { null }, existingAudioUrl = audioUrl.value.trim(), server = server, - quality = MediaCompressor.intToCompressorQuality(mediaQualitySlider.value), + quality = CompressorQuality.fromSlider(mediaQualitySlider.value), stripMetadata = stripMetadata.value, - appContext = context.applicationContext, + uploader = accountViewModel.host.mediaUploader, ) isSending.value = true @@ -265,9 +265,9 @@ class NewPodcastEpisodeViewModel : ViewModel() { val existingCoverUrl: String?, val existingAudioUrl: String, val server: ServerName, - val quality: com.vitorpamplona.amethyst.service.uploads.CompressorQuality, + val quality: CompressorQuality, val stripMetadata: Boolean, - val appContext: Context, + val uploader: MediaUploader, ) private suspend fun performParallelUploads(snapshot: Snapshot): Pair = @@ -294,7 +294,7 @@ class NewPodcastEpisodeViewModel : ViewModel() { quality = snapshot.quality, stripMetadata = snapshot.stripMetadata, alt = snapshot.title.ifBlank { null }, - context = snapshot.appContext, + uploader = snapshot.uploader, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/PodcastAudioProbe.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/PodcastAudioProbe.kt new file mode 100644 index 0000000000..2d1847a7c2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/PodcastAudioProbe.kt @@ -0,0 +1,51 @@ +/* + * 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.podcasts.authoring + +import android.content.Context +import android.media.MediaMetadataRetriever +import android.net.Uri +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.PodcastComposerMedia + +object PodcastAudioProbe { + /** + * Reads duration + title from a picked audio file via [MediaMetadataRetriever] (heavy — call off + * the main thread). Returns null if the provider rejects it or the file isn't a real container. + */ + fun probeAudio( + context: Context, + uri: Uri, + ): PodcastComposerMedia.ProbedAudio? { + val retriever = MediaMetadataRetriever() + return try { + retriever.setDataSource(context, uri) + val durationMs = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull() + PodcastComposerMedia.ProbedAudio( + durationSeconds = durationMs?.let { (it / 1000).toInt().takeIf { secs -> secs > 0 } }, + title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)?.trim()?.ifBlank { null }, + ) + } catch (_: Exception) { + null + } finally { + retriever.release() + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt index fcd6571a7a..82091a5d8a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt @@ -35,9 +35,9 @@ import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserTagFollows import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag.HashtagHeader import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagHeader @Composable fun TabFollowedTags( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentTargetsDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentTargetsDialog.kt index d878498609..bf49de7a0a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentTargetsDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentTargetsDialog.kt @@ -66,9 +66,9 @@ import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.ErrorMessageDialog +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt index 49f43cad99..cc5d516355 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt @@ -49,7 +49,6 @@ import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @@ -60,6 +59,8 @@ import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserPicture import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.upload_image import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.commons.ui.actions.NewUserMetadataViewModel +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture @@ -69,8 +70,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.ui.theme.userProfileBorderModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.UserExternalIdentitiesViewModel -import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel import kotlinx.coroutines.launch @@ -194,7 +193,6 @@ private fun ProfilePictureUploadButton( ) { val postViewModel: NewUserMetadataViewModel = viewModel() postViewModel.init(accountViewModel) - val context = LocalContext.current var showGallerySelect by remember { mutableStateOf(false) } if (showGallerySelect) { @@ -202,7 +200,7 @@ private fun ProfilePictureUploadButton( onImageUri = { media -> showGallerySelect = false if (media != null) { - postViewModel.uploadPictureAndSave(media, context, accountViewModel.toastManager::toast) + postViewModel.uploadPictureAndSave(media, accountViewModel.host.mediaUploader, accountViewModel.toastManager::toast) } }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt index 045e37ad70..f30f5a1bbe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt @@ -105,13 +105,13 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackBu import com.vitorpamplona.amethyst.commons.ui.note.LoadUser import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.FeeTier +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.rateFor import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.ClinkOfferPayer import com.vitorpamplona.amethyst.ui.note.payViaIntent -import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.FeeTier -import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.rateFor import com.vitorpamplona.quartz.experimental.clink.common.SatRange import com.vitorpamplona.quartz.experimental.clink.offers.OfferErrorCode import com.vitorpamplona.quartz.experimental.clink.pointers.ClinkPointerParser diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ScanQrImageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ScanQrImageScreen.kt index 66e4b35358..7563df5eed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ScanQrImageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ScanQrImageScreen.kt @@ -56,11 +56,11 @@ import com.vitorpamplona.amethyst.commons.resources.scan_qr import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.uriToRoute import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.scanner.QrImageImport import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.scanner.ZxingCppBarcodeDecoder import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.scanner.copyToClipboard -import com.vitorpamplona.amethyst.ui.uriToRoute import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerDialog.kt index e05e6246cd..ddb6f718ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerDialog.kt @@ -104,9 +104,10 @@ import com.vitorpamplona.amethyst.commons.resources.qr_scanner_grant_camera import com.vitorpamplona.amethyst.commons.resources.qr_scanner_no_code_in_image import com.vitorpamplona.amethyst.commons.resources.qr_scanner_open_settings import com.vitorpamplona.amethyst.commons.resources.qr_scanner_unavailable +import com.vitorpamplona.amethyst.commons.ui.components.SetDialogToEdgeToEdge +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.ScanOutcome import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.ui.call.openAppSettings -import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay @@ -114,15 +115,6 @@ import kotlinx.coroutines.launch import java.util.concurrent.Executors import java.util.concurrent.TimeUnit -/** What the caller did with a decoded payload — and therefore what the scanner does next. */ -enum class ScanOutcome { - /** The caller acted on it. Close the scanner. */ - Handled, - - /** Decoded fine, but this screen has nothing to do with it. Explain, and keep scanning. */ - NotSupported, -} - /** * Full-screen QR scanner. * diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index 19d92b8115..abbf158297 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -90,6 +90,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.RowColSpacing @@ -104,7 +105,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.broadcast.renderBroa import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayExporter import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayListCollection import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayZipExporter -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.ConnectedRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.renderConnectedItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.DMRelayListViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt index e8151d24fe..976150bbe1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt @@ -33,16 +33,16 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder @Composable fun BlockedRelayList( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt index 9c34b539c1..127c8df427 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt @@ -33,16 +33,16 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder @Composable fun BroadcastRelayList( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt index 1f440f217f..43964a6b3a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt @@ -28,6 +28,8 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoClickableRow import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt index 7d565eef51..b91d5bb6fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt @@ -22,11 +22,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relays.ui.CountFilter import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.util.replace import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.count @@ -131,7 +132,7 @@ abstract class BasicRelaySetupInfoModel : ViewModel() { relaySetupInfoBuilder( normalized = it, forcesTor = - Amethyst.instance.torEvaluatorFlow.flow.value + accountViewModel.host.torRelayEvaluation.value .useTor(it), ) }.distinctBy { it.relay } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayListCollection.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayListCollection.kt index 1406ded079..e8e986423e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayListCollection.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayListCollection.kt @@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.commons.resources.search_section import com.vitorpamplona.amethyst.commons.resources.search_section_explainer import com.vitorpamplona.amethyst.commons.resources.trusted_section import com.vitorpamplona.amethyst.commons.resources.trusted_section_explainer +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import org.jetbrains.compose.resources.StringResource data class RelayListCollection( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayZipExporter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayZipExporter.kt index ca55f16c25..5bb043eb06 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayZipExporter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayZipExporter.kt @@ -26,6 +26,7 @@ import androidx.core.content.FileProvider import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.export_relay_settings import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import java.io.File import java.io.FileOutputStream import java.util.zip.ZipEntry diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt index 062c3c4b3c..b926c58619 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt @@ -41,12 +41,12 @@ import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.active_subs_title import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt index 8e7d24af54..23092ef734 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt @@ -21,9 +21,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.cache.LocalCache -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -36,7 +35,7 @@ class ConnectedRelayListViewModel : BasicRelaySetupInfoModel() { return relayList .map { - val reqs = Amethyst.instance.client.activeRequests(it) + val reqs = account.client.activeRequests(it) val users = mutableSetOf() reqs.forEach { _, filters -> @@ -55,9 +54,9 @@ class ConnectedRelayListViewModel : BasicRelaySetupInfoModel() { BasicRelaySetupInfo( relay = it, - relayStat = Amethyst.instance.relayStats.get(it), + relayStat = accountViewModel.host.relayStats.get(it), forcesTor = - Amethyst.instance.torEvaluatorFlow.flow.value + accountViewModel.host.torRelayEvaluation.value .useTor(it), users = users.mapNotNull { hex -> LocalCache.checkGetOrCreateUser(hex) }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt index 0da5a916ab..fa587725a0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt @@ -43,14 +43,14 @@ import com.vitorpamplona.amethyst.commons.resources.default_relays_longer import com.vitorpamplona.amethyst.commons.resources.dm_relays_not_found_editing import com.vitorpamplona.amethyst.commons.resources.dm_relays_not_found_examples2 import com.vitorpamplona.amethyst.commons.resources.dm_relays_title +import com.vitorpamplona.amethyst.commons.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt index 33f942ae34..8b5680b539 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt @@ -34,16 +34,16 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt index 31e185a97d..85949dcc17 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt @@ -33,16 +33,16 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder @Composable fun FavoriteRelayList( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt index cee70e71e7..5360bd9692 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt @@ -34,16 +34,16 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListView.kt index 2ab30391a2..8b6e3ad4a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/keyPackage/KeyPackageRelayListView.kt @@ -34,16 +34,16 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt index e2237a6b2c..3580d9c981 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt @@ -37,6 +37,10 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.send_kind0_to_local_relay_description import com.vitorpamplona.amethyst.commons.resources.send_kind0_to_local_relay_title import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding @@ -44,11 +48,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategorySpacingWithHo import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow @Composable fun LocalRelayList( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt index 9dd50e8875..05e0fa25a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt @@ -34,16 +34,16 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt index 11942cfbca..36e17be0e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt @@ -34,16 +34,16 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt index fa8c1450ca..5f7fa37608 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt @@ -29,10 +29,10 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.events_from_you import com.vitorpamplona.amethyst.commons.resources.events_to_you +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.util.replace import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.count import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt index 7b69acfd25..88c6e2cf56 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt @@ -137,6 +137,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.ObserveAndRenderNIP05VerifiedS import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.AnimateOnNewSearch import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.BackButton import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.kindDisplayName import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness @@ -147,7 +148,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SmallBorder import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.nip05 import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton import com.vitorpamplona.quartz.kinds.KindNames import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt index 7c1fde3709..bb6f268bb5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt @@ -34,16 +34,16 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt index 2a3ce83c58..0b7cdd27a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt @@ -34,16 +34,16 @@ import com.vitorpamplona.amethyst.commons.relays.ui.RelayCountResult import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt index ef75453d74..488797bcfc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt @@ -92,18 +92,18 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_group_fallback_name import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName +import com.vitorpamplona.amethyst.commons.ui.platform.rememberConcordImageModel +import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings +import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.RelayIconFilter import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.warningColor import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName -import com.vitorpamplona.amethyst.ui.pluralStringRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.loadMarmotRelayIcon import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.rememberMarmotGroupAvatarUrl -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.rememberConcordImageModel -import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl @@ -127,7 +127,7 @@ fun ActiveSubscriptionsScreen( nav: INav, viewModel: ActiveSubscriptionsViewModel = viewModel(), ) { - LaunchedEffect(Unit) { viewModel.startPolling() } + LaunchedEffect(Unit) { viewModel.pollWhileShown(accountViewModel.account.client) } val state by viewModel.state.collectAsStateWithLifecycle() Scaffold( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsViewModel.kt index a5db379f0b..5e79ff23c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsViewModel.kt @@ -22,20 +22,18 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.subscriptions import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavPerRelayFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** @@ -142,19 +140,20 @@ class ActiveSubscriptionsViewModel : ViewModel() { private val _state = MutableStateFlow(ActiveSubscriptionsState()) val state: StateFlow = _state.asStateFlow() - /** Polls while the screen is on. [REFRESH_MS] is slow enough to be free, fast enough to feel live. */ - fun startPolling() { - viewModelScope.launch(Dispatchers.Default) { - while (isActive) { - _state.value = snapshot() - kotlinx.coroutines.delay(REFRESH_MS) - } + /** + * Polls until the caller's scope is cancelled. Run it from the screen's effect, so leaving the + * screen stops it; a loop in `viewModelScope` would outlive the screen, and each return to it + * would stack another. [REFRESH_MS] is slow enough to be free, fast enough to feel live. + */ + suspend fun pollWhileShown(client: INostrClient) { + while (true) { + _state.value = snapshot(client) + delay(REFRESH_MS) } } - private suspend fun snapshot(): ActiveSubscriptionsState = + private suspend fun snapshot(client: INostrClient): ActiveSubscriptionsState = withContext(Dispatchers.Default) { - val client = Amethyst.instance.client aggregateSubscriptions( client.connectedRelaysFlow().value.associateWith { relay -> client.activeRequests(relay).values.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt index 78193498cb..c96d2c8707 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt @@ -33,16 +33,16 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relays.ui.RelayDragState import com.vitorpamplona.amethyst.commons.relays.ui.rememberRelayDragState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder @Composable fun TrustedRelayList( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt index f51e300fc1..6c2b5e7171 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt @@ -95,6 +95,8 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.note.DateTimeStyle import com.vitorpamplona.amethyst.commons.ui.note.formatDateTime +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.HorzHalfVertPadding @@ -102,8 +104,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt index b84a61e452..bb8fe5a062 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt @@ -43,14 +43,14 @@ import com.vitorpamplona.amethyst.commons.resources.default_relays_longer import com.vitorpamplona.amethyst.commons.resources.search_relays_not_found_editing import com.vitorpamplona.amethyst.commons.resources.search_relays_not_found_examples import com.vitorpamplona.amethyst.commons.resources.search_relays_title +import com.vitorpamplona.amethyst.commons.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search.SearchRelayList import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search.SearchRelayListViewModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt index 91ea210353..57280cf21e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt @@ -48,7 +48,7 @@ import com.vitorpamplona.amethyst.commons.search.SearchSource import com.vitorpamplona.amethyst.commons.search.SearchState import com.vitorpamplona.amethyst.commons.search.wholeInputNip19 import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.userUriPrefixes -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.quartz.buzz.invite.BuzzInviteLink import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.core.toHexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt index f6ef0c23c0..2e0c06fa66 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt @@ -117,6 +117,7 @@ import com.vitorpamplona.amethyst.commons.search.QuerySerializer import com.vitorpamplona.amethyst.commons.search.SearchScope import com.vitorpamplona.amethyst.commons.search.SearchSortOrder import com.vitorpamplona.amethyst.commons.search.SearchSource +import com.vitorpamplona.amethyst.commons.ui.components.namecoin.NamecoinResolutionRow import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding @@ -130,6 +131,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowU import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoClickableRow import com.vitorpamplona.amethyst.commons.ui.search.GroupCandidate import com.vitorpamplona.amethyst.commons.ui.search.SEARCH_PICKER_LIMIT import com.vitorpamplona.amethyst.commons.ui.search.SearchFieldState @@ -145,9 +147,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.service.location.CachedReversedGeoLocations import com.vitorpamplona.amethyst.service.relayClient.searchCommand.TextSearchDataSourceSubscription -import com.vitorpamplona.amethyst.ui.components.namecoin.NamecoinResolutionRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChannelName -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoClickableRow import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers 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 f7ff0a81e3..5fbd6cc27d 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 @@ -30,22 +30,17 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.SegmentedButton -import androidx.compose.material3.SegmentedButtonDefaults -import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -61,7 +56,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextOverflow @@ -122,11 +116,15 @@ import com.vitorpamplona.amethyst.commons.resources.ui_feature_set_type_simplifi import com.vitorpamplona.amethyst.commons.resources.ui_style import com.vitorpamplona.amethyst.commons.resources.ui_style_description import com.vitorpamplona.amethyst.commons.ui.components.SpinnerSelectionDialog -import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SegmentedChoiceTile +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsBlockTile +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsControlRow +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsDivider +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsSection import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.ui.theme.contentColorOnAccent @@ -134,7 +132,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.isLight import com.vitorpamplona.amethyst.commons.ui.theme.previewColor import com.vitorpamplona.amethyst.commons.ui.theme.toFontFamily import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap @@ -219,53 +216,6 @@ fun SettingsScreen( } } -/** - * A [SettingsBlockTile] whose control is a full-width [SingleChoiceSegmentedButtonRow]. - * This is the in-screen replacement for the old dropdown ([TextSpinner]) rows: every - * option is visible and one tap away. Best for 2–4 mutually-exclusive options. - * - * [optionTextStyle] lets each option render its own label preview — e.g. the font tiles - * draw each label in the very typeface / size it selects, so the row demonstrates the - * choices instead of only naming them. - */ -@Composable -internal fun SegmentedChoiceTile( - icon: MaterialSymbol, - title: StringResource, - description: StringResource, - options: List, - labelRes: (T) -> StringResource, - selected: T, - onSelect: (T) -> Unit, - optionTextStyle: (@Composable (T) -> TextStyle)? = null, -) { - SettingsBlockTile( - icon = icon, - title = stringRes(title), - description = stringRes(description), - ) { - SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { - options.forEachIndexed { index, option -> - SegmentedButton( - selected = option == selected, - onClick = { onSelect(option) }, - shape = SegmentedButtonDefaults.itemShape(index = index, count = options.size), - // Drop the default check icon: the fill already signals selection, and the icon - // steals ~24dp that the label needs in a 3–4-up row. - icon = {}, - ) { - Text( - text = stringRes(labelRes(option)), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = optionTextStyle?.invoke(option) ?: LocalTextStyle.current, - ) - } - } - } - } -} - /** A [SettingsControlRow] with a [Switch] backed by a [BooleanType] flow (ALWAYS ⇔ on). */ @Composable private fun BooleanSwitchTile( @@ -617,91 +567,3 @@ private fun AccentColorSwatch( } } } - -@Composable -fun SettingsRow( - name: StringResource, - description: StringResource, - selectedItems: ImmutableList, - selectedIndex: Int, - onSelect: (Int) -> Unit, -) { - SettingsRow(name, description) { - TextSpinner( - label = "", - placeholder = selectedItems[selectedIndex].title, - options = selectedItems, - onSelect = onSelect, - modifier = Modifier.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)), - ) - } -} - -@Composable -fun SettingsRow( - name: StringResource, - description: StringResource, - modifier: Modifier = Modifier.fillMaxWidth(), - content: @Composable () -> Unit, -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(5.dp), - ) { - Column( - modifier = Modifier.weight(2.0f), - verticalArrangement = Arrangement.spacedBy(3.dp), - ) { - Text( - text = stringRes(name), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = stringRes(description), - style = MaterialTheme.typography.bodySmall, - color = Color.Gray, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - ) - } - - Column( - Modifier.weight(1f), - horizontalAlignment = Alignment.End, - ) { - content() - } - } -} - -@Composable -fun SettingsRow( - name: StringResource, - description: StringResource, -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(5.dp), - ) { - Column( - modifier = Modifier.weight(2.0f), - verticalArrangement = Arrangement.spacedBy(3.dp), - ) { - Text( - text = stringRes(name), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = stringRes(description), - style = MaterialTheme.typography.bodySmall, - color = Color.Gray, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - ) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt index 38f77ce538..d6067bc225 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ComposeSettingsScreen.kt @@ -96,6 +96,12 @@ import com.vitorpamplona.amethyst.commons.service.pow.PoWCategory import com.vitorpamplona.amethyst.commons.service.pow.PoWEstimator import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsBlockTile +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsControlRow +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsDivider +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsSection +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsStepper +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsSubControlRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt index 5a62043f6c..111c7bf346 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt @@ -52,6 +52,7 @@ import com.vitorpamplona.amethyst.commons.resources.payment_targets_section_expl import com.vitorpamplona.amethyst.commons.resources.wallet_connect import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.WalletViewModel import com.vitorpamplona.amethyst.commons.ui.settings.SettingsCategory import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategorySpacingModifier @@ -63,7 +64,6 @@ import com.vitorpamplona.amethyst.ui.actions.paymentTargets.PaymentTargetsViewMo import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountContent import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory -import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletViewModel import com.vitorpamplona.quartz.nipA3PaymentTargets.PaymentTarget @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NotificationSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NotificationSettingsScreen.kt index 22cdf51516..100380e1fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NotificationSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NotificationSettingsScreen.kt @@ -89,6 +89,10 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackBu import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.commons.ui.note.LoadUser import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsDivider +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsItem +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsSection +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsSwitchTile import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ResourceUsageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ResourceUsageScreen.kt index 84d81bfe39..e20138f7ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ResourceUsageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ResourceUsageScreen.kt @@ -138,6 +138,8 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeToMessage import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsDivider +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsSection import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor import com.vitorpamplona.amethyst.commons.ui.theme.warningColor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedView.kt index 3851b8fd57..fd87802cd2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedView.kt @@ -42,6 +42,7 @@ import com.vitorpamplona.amethyst.commons.resources.refresh import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.StringFeedViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt index fc09ddd1dd..0b8f3f2a38 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt @@ -84,6 +84,7 @@ import com.vitorpamplona.amethyst.commons.resources.user_preferences import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnBackupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnBackupScreen.kt index 8aac8ee458..2844f928ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnBackupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnBackupScreen.kt @@ -77,9 +77,9 @@ import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable_deta import com.vitorpamplona.amethyst.commons.ui.components.EmptyState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsSection import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSection import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnMigrateScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnMigrateScreen.kt index c2bf67e6fb..0bc29f3779 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnMigrateScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnMigrateScreen.kt @@ -82,12 +82,12 @@ import com.vitorpamplona.amethyst.commons.ui.components.EmptyState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsSection import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.model.cordn.AndroidCordnBlobStore -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSection import com.vitorpamplona.quartz.cordn.appMultiDevice.CordnHandoffCode import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt index 4abf06c4dd..234c89f792 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt @@ -52,6 +52,8 @@ import com.vitorpamplona.amethyst.commons.resources.ic_compose import com.vitorpamplona.amethyst.commons.resources.new_short_video import com.vitorpamplona.amethyst.commons.resources.record_a_video import com.vitorpamplona.amethyst.commons.resources.upload_image +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -60,8 +62,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.NewMediaModel import com.vitorpamplona.amethyst.ui.actions.NewMediaView -import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelect -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideo import com.vitorpamplona.amethyst.ui.actions.uploads.resolveSharedMedia import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt index 3dff6d975c..197918f1a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt @@ -51,9 +51,9 @@ import com.vitorpamplona.amethyst.commons.ui.components.ContentWarningGate import com.vitorpamplona.amethyst.commons.ui.components.collectContentWarningReasons import com.vitorpamplona.amethyst.commons.ui.components.mediaSizingModifier import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.components.ZoomableContentView -import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.UserCardHeader import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip31Alts.alt 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 9f839d5c35..e22773d3c0 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 @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview -import android.annotation.SuppressLint import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable @@ -144,6 +143,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.commons.ui.note.nip22Comments.DisplayCommentScope import com.vitorpamplona.amethyst.commons.ui.note.observeEdits +import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.ui.note.types.BadgeDisplay import com.vitorpamplona.amethyst.commons.ui.note.types.DisplayBlockedRelayList import com.vitorpamplona.amethyst.commons.ui.note.types.DisplayBroadcastRelayList @@ -262,7 +262,6 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.thread.LevelFeedViewModel import com.vitorpamplona.amethyst.ui.components.ZoomableContentView -import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.amethyst.ui.note.types.AudioHeader import com.vitorpamplona.amethyst.ui.note.types.AudioTrackHeader import com.vitorpamplona.amethyst.ui.note.types.RenderGitIssueEvent @@ -1286,7 +1285,7 @@ private fun RenderApprovalIfNeeded( } } -@SuppressLint("StateFlowValueCalledInComposition") +@Suppress("StateFlowValueCalledInComposition") @Composable private fun RenderClassifiedsReaderForThread( noteEvent: ClassifiedsEvent, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt index 3eb6c6171f..e5edcfb2be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt @@ -31,10 +31,10 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.threadview.dal.ThreadFeedViewModel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.threadview.datasources.ThreadFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.ThreadFeedViewModel @Composable fun ThreadScreen( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt index 2844a3f4a2..1d10a9eab3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlPostScreen.kt @@ -27,8 +27,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel import com.vitorpamplona.amethyst.ui.note.nip22Comments.GenericCommentPostScreen diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlScreen.kt index b6c5d6bb07..0e42872c12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlScreen.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.url -import android.annotation.SuppressLint import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -59,7 +58,7 @@ fun UrlScreen( PrepareViewModelsUrlScreen(url, accountViewModel, nav) } -@SuppressLint("StateFlowValueCalledInComposition") +@Suppress("StateFlowValueCalledInComposition") @Composable fun PrepareViewModelsUrlScreen( url: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/datasource/UrlFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/datasource/UrlFilterAssemblerSubscription.kt index 03aa1ae197..4864613b42 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/datasource/UrlFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/datasource/UrlFilterAssemblerSubscription.kt @@ -20,14 +20,13 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.url.datasource -import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription import com.vitorpamplona.amethyst.commons.relayClient.url.UrlQueryState import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -@SuppressLint("StateFlowValueCalledInComposition") +@Suppress("StateFlowValueCalledInComposition") @Composable fun UrlFilterAssemblerSubscription( url: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileMetadataCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileMetadataCardCompose.kt index c88395ced3..aa5b9947f3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileMetadataCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileMetadataCardCompose.kt @@ -45,11 +45,11 @@ import com.vitorpamplona.amethyst.commons.ui.components.ContentWarningGate import com.vitorpamplona.amethyst.commons.ui.components.collectContentWarningReasons import com.vitorpamplona.amethyst.commons.ui.components.mediaSizingModifier import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.ui.note.types.FileMetadataAttachmentCard import com.vitorpamplona.amethyst.commons.ui.note.types.toMediaContent import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.components.ZoomableContentView -import com.vitorpamplona.amethyst.ui.note.ReactionsRow import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt index 905532836c..fa0450900f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt @@ -52,6 +52,8 @@ import com.vitorpamplona.amethyst.commons.resources.new_media import com.vitorpamplona.amethyst.commons.resources.record_a_video import com.vitorpamplona.amethyst.commons.resources.take_a_picture import com.vitorpamplona.amethyst.commons.resources.upload_image +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -60,8 +62,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.NewMediaModel import com.vitorpamplona.amethyst.ui.actions.NewMediaView -import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelect -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.TakePicture import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideo import com.vitorpamplona.amethyst.ui.actions.uploads.resolveSharedMedia diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/NewWorkoutScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/NewWorkoutScreen.kt index 8a168763c0..211417c48e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/NewWorkoutScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/NewWorkoutScreen.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts -import androidx.activity.compose.BackHandler import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -70,6 +69,7 @@ import com.vitorpamplona.amethyst.commons.resources.workout_minutes import com.vitorpamplona.amethyst.commons.resources.workout_notes import com.vitorpamplona.amethyst.commons.resources.workout_seconds import com.vitorpamplona.amethyst.commons.resources.workout_title +import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar @@ -91,7 +91,7 @@ fun NewWorkoutScreen( postViewModel.init(accountViewModel) postViewModel.prefill(prefill) - BackHandler { + PlatformBackHandler { postViewModel.cancel() nav.popBack() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextField.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextField.kt index f29c9a28de..93396f75ff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextField.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextField.kt @@ -53,9 +53,9 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_qr_code import com.vitorpamplona.amethyst.commons.resources.nsec_npub_hex_private_key import com.vitorpamplona.amethyst.commons.resources.show_password import com.vitorpamplona.amethyst.commons.ui.painterRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner @Composable fun KeyTextField( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 2d01d2adab..04a3631842 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.LocalInlineQuoteRenderer import com.vitorpamplona.amethyst.commons.ui.components.LocalTranslationPlatform import com.vitorpamplona.amethyst.commons.ui.note.platform.LocalNotePlatform import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppPlatform +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextPlatform import com.vitorpamplona.amethyst.commons.ui.screen.DisplaySettings import com.vitorpamplona.amethyst.commons.ui.screen.collectDisplaySettings @@ -57,6 +58,7 @@ import com.vitorpamplona.amethyst.ui.components.DefaultInlineQuoteRenderer import com.vitorpamplona.amethyst.ui.components.FlavorTranslationPlatform import com.vitorpamplona.amethyst.ui.note.platform.AndroidNotePlatform import com.vitorpamplona.amethyst.ui.platform.AndroidAppPlatform +import com.vitorpamplona.amethyst.ui.platform.AndroidAppServices val chartLightColors = VicoTheme( @@ -141,6 +143,7 @@ fun AmethystTheme( LocalTranslationPlatform provides FlavorTranslationPlatform, LocalNotePlatform provides AndroidNotePlatform, LocalAppPlatform provides AndroidAppPlatform, + LocalAppServices provides AndroidAppServices, content = content, ) } 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 01ef24d512..7621bdf3c1 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 @@ -85,14 +85,14 @@ import com.vitorpamplona.amethyst.commons.tor.explainerId import com.vitorpamplona.amethyst.commons.tor.parseTorPresetType import com.vitorpamplona.amethyst.commons.tor.parseTorType import com.vitorpamplona.amethyst.commons.tor.resourceId +import com.vitorpamplona.amethyst.commons.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CancellationException import org.jetbrains.compose.resources.StringResource diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt index 9f5d9f0087..9bc5ac07a9 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt @@ -30,7 +30,6 @@ import com.google.mlkit.nl.translate.TranslateLanguage import com.google.mlkit.nl.translate.Translation import com.google.mlkit.nl.translate.Translator import com.google.mlkit.nl.translate.TranslatorOptions -import com.vitorpamplona.amethyst.service.checkNotInMainThread import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -91,7 +90,6 @@ object LanguageTranslatorService { source: String, target: String, ): Task { - checkNotInMainThread() val sourceLangCode = TranslateLanguage.fromLanguageTag(source) val targetLangCode = TranslateLanguage.fromLanguageTag(target) @@ -110,8 +108,6 @@ object LanguageTranslatorService { val translator = translators[options] return translator.downloadModelIfNeeded().onSuccessTask(executorService) { - checkNotInMainThread() - val dict = TranslationDictionary.build(text) val encoded = TranslationDictionary.encode(text, dict) diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/URIParserTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/URIParserTest.kt index 1cf26a6e0c..503f78f194 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/URIParserTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/URIParserTest.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst import com.vitorpamplona.amethyst.commons.model.navigation.Route -import com.vitorpamplona.amethyst.ui.connectedAppRoute -import com.vitorpamplona.amethyst.ui.isConnectedAppRoute -import com.vitorpamplona.amethyst.ui.urlRoute +import com.vitorpamplona.amethyst.commons.ui.connectedAppRoute +import com.vitorpamplona.amethyst.commons.ui.isConnectedAppRoute +import com.vitorpamplona.amethyst.commons.ui.urlRoute import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/favorites/RecentSubtitleTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/favorites/RecentSubtitleTest.kt index 988495bced..21956e5607 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/favorites/RecentSubtitleTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/favorites/RecentSubtitleTest.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.favorites -import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.recentSubtitle +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.browser.recentSubtitle import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Test diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/BottomBarCategoriesTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/BottomBarCategoriesTest.kt index f6ce46fa14..c91d4ec94a 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/BottomBarCategoriesTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/BottomBarCategoriesTest.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.navigation -import com.vitorpamplona.amethyst.ui.navigation.bottombars.BottomBarCategories -import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarCatalog +import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.BottomBarCategories +import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.NavBarCatalog import org.junit.Assert.assertEquals import org.junit.Test diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/BottomBarSettingsStateTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/BottomBarSettingsStateTest.kt index 9d8beb9dd6..5a9b8e7c9c 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/BottomBarSettingsStateTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/BottomBarSettingsStateTest.kt @@ -24,8 +24,8 @@ import com.vitorpamplona.amethyst.commons.model.navigation.BottomBarEntry import com.vitorpamplona.amethyst.commons.model.navigation.DefaultBottomBarEntries import com.vitorpamplona.amethyst.commons.model.navigation.NavBarItem import com.vitorpamplona.amethyst.commons.model.navigation.stableKey -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.BottomBarEditing -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.BottomBarSettingsState +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.BottomBarEditing +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.BottomBarSettingsState import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/DrawerItemVisibilityTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/DrawerItemVisibilityTest.kt index 11ecb7f0f8..6da8a404e3 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/DrawerItemVisibilityTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/DrawerItemVisibilityTest.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.navigation import com.vitorpamplona.amethyst.commons.model.navigation.DrawerItemVisibility import com.vitorpamplona.amethyst.commons.model.navigation.DrawerSectionId import com.vitorpamplona.amethyst.commons.model.navigation.NavBarItem -import com.vitorpamplona.amethyst.ui.navigation.drawer.DrawerSectionVisibility -import com.vitorpamplona.amethyst.ui.navigation.drawer.DrawerSections -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.DrawerSettingsState +import com.vitorpamplona.amethyst.commons.ui.navigation.drawer.DrawerSectionVisibility +import com.vitorpamplona.amethyst.commons.ui.navigation.drawer.DrawerSections +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.DrawerSettingsState import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/DrawerSectionsTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/DrawerSectionsTest.kt index 49b0ed57b0..537e1999e0 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/DrawerSectionsTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/DrawerSectionsTest.kt @@ -24,9 +24,9 @@ import com.vitorpamplona.amethyst.commons.model.navigation.DrawerSectionId import com.vitorpamplona.amethyst.commons.model.navigation.MandatoryDrawerItems import com.vitorpamplona.amethyst.commons.model.navigation.drawerSectionIdsFromNames import com.vitorpamplona.amethyst.commons.model.navigation.toNames -import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarCatalog -import com.vitorpamplona.amethyst.ui.navigation.drawer.DrawerSections -import com.vitorpamplona.amethyst.ui.navigation.drawer.SdkGatedDrawerItems +import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.NavBarCatalog +import com.vitorpamplona.amethyst.commons.ui.navigation.drawer.DrawerSections +import com.vitorpamplona.amethyst.commons.ui.navigation.drawer.SdkGatedDrawerItems import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/notifications/NotificationDeepLinkTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/notifications/NotificationDeepLinkTest.kt index 7c7a87114e..3aeff8efba 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/notifications/NotificationDeepLinkTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/notifications/NotificationDeepLinkTest.kt @@ -24,14 +24,14 @@ import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.navigation.routeFor +import com.vitorpamplona.amethyst.commons.ui.chatroomRoute +import com.vitorpamplona.amethyst.commons.ui.isChatroomRoute +import com.vitorpamplona.amethyst.commons.ui.isPrivateNoteRoute +import com.vitorpamplona.amethyst.commons.ui.navigation.findQueryParameterValue import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor -import com.vitorpamplona.amethyst.ui.chatroomRoute -import com.vitorpamplona.amethyst.ui.isChatroomRoute -import com.vitorpamplona.amethyst.ui.isPrivateNoteRoute +import com.vitorpamplona.amethyst.commons.ui.privateNoteRoute +import com.vitorpamplona.amethyst.commons.ui.uriToRoute import com.vitorpamplona.amethyst.ui.navigation.findParameterValue -import com.vitorpamplona.amethyst.ui.navigation.findQueryParameterValue -import com.vitorpamplona.amethyst.ui.privateNoteRoute -import com.vitorpamplona.amethyst.ui.uriToRoute import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.people.PTag diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/resourceusage/ResourceUsageLedgerTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/resourceusage/ResourceUsageLedgerTest.kt index 6f0365319d..93055997bf 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/resourceusage/ResourceUsageLedgerTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/resourceusage/ResourceUsageLedgerTest.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.resourceusage import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose +import com.vitorpamplona.amethyst.commons.service.resourceusage.UsageKeys import com.vitorpamplona.amethyst.service.playback.playerPool.MediaPlayTimeTracker import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/MediaMimeTypesTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/MediaMimeTypesTest.kt index bbe0a2c08e..7af7a5dd44 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/MediaMimeTypesTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/MediaMimeTypesTest.kt @@ -20,6 +20,10 @@ */ package com.vitorpamplona.amethyst.service.uploads +import com.vitorpamplona.amethyst.commons.service.uploads.AVIF_EXTENSION +import com.vitorpamplona.amethyst.commons.service.uploads.AVIF_MIME +import com.vitorpamplona.amethyst.commons.service.uploads.extensionFromMimeType +import com.vitorpamplona.amethyst.commons.service.uploads.isAvif import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploaderExtensionTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploaderExtensionTest.kt index 951779323d..f65547d8f3 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploaderExtensionTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploaderExtensionTest.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.uploads.blossom -import com.vitorpamplona.amethyst.service.uploads.extensionFromMimeType +import com.vitorpamplona.amethyst.commons.service.uploads.extensionFromMimeType import org.junit.Assert.assertEquals import org.junit.Test diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/UriToRouteTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/UriToRouteTest.kt index 8ba0523bc1..1f242080d7 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/UriToRouteTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/UriToRouteTest.kt @@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.ui import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.navigation.Route +import com.vitorpamplona.amethyst.commons.ui.fragmentHashtagOrNull +import com.vitorpamplona.amethyst.commons.ui.uriToRoute import io.mockk.every import io.mockk.mockk import org.junit.Assert.assertEquals diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectedMediaTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectedMediaTest.kt index 7d8d421590..59a2470b09 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectedMediaTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectedMediaTest.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.actions.uploads import android.net.Uri +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import io.mockk.mockk import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/MediaCompressorTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/MediaCompressorTest.kt index b0fd8b73c5..9ff95b81a9 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/MediaCompressorTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/MediaCompressorTest.kt @@ -24,7 +24,7 @@ import android.content.Context import android.net.Uri import android.os.Looper import com.davotoula.lightcompressor.VideoCompressor -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.ui.components.util.MediaCompressorFileUtils import id.zelory.compressor.Compressor diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/namecoin/NamecoinResolutionRowTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/namecoin/NamecoinResolutionRowTest.kt index 6436ea254f..5e7e0f4a10 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/namecoin/NamecoinResolutionRowTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/namecoin/NamecoinResolutionRowTest.kt @@ -21,6 +21,8 @@ package com.vitorpamplona.amethyst.ui.components.namecoin import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.namecoin.NamecoinResolveState +import com.vitorpamplona.amethyst.commons.ui.components.namecoin.looksLikeNamecoinIdentifier +import com.vitorpamplona.amethyst.commons.ui.components.namecoin.mapOutcomeToResolveState import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNostrResult import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinResolveOutcome import org.junit.Assert.assertEquals diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupPresentationLabelsTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupPresentationLabelsTest.kt index bd28de71ee..3c6b83f3b6 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupPresentationLabelsTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupPresentationLabelsTest.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.backups +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups.labelsWrittenBy +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups.presentationOf import com.vitorpamplona.quartz.nip01Core.diff.EventDiff import com.vitorpamplona.quartz.nip01Core.diff.ListDiff import com.vitorpamplona.quartz.nip01Core.metadata.MetadataDiff diff --git a/commons/plans/2026-09-27-one-ui-android-desktop.md b/commons/plans/2026-09-27-one-ui-android-desktop.md index 2ee47b6ae1..cfdef91a50 100644 --- a/commons/plans/2026-09-27-one-ui-android-desktop.md +++ b/commons/plans/2026-09-27-one-ui-android-desktop.md @@ -119,7 +119,7 @@ Each leaving edge is a seam to cut before the group can move. | `Amethyst.kt` | `keyCache` (Account), `encryptedStorage` (Geohash), `appContext` (Nip46 bridge), `notificationDispatcher` (EventProcessor) | constructor parameters / ports; the notification dispatcher gets an interface. **Done 2026-09-28**: `keyCache` → `Account.encryptionKeyCache`; the notifications → `MarmotGroupNotifier`, which `NotificationDispatcher` implements; `encryptedStorage` and `appContext` left with the Geohash and Nip46 cuts. | | `LocalPreferences.kt` | `saveToEncryptedStorage(accountSettings)` on settings change | **Done 2026-09-28** as a constructor lambda, `Account.saveSettings: suspend (AccountSettings) -> Unit`, rather than a named port. | | `DebugUtils.kt` | `logTime` (2 sites) | **Done 2026-09-28.** `commons/util` already had an identical `logTime`; its Android `isDebug` was hard-coded `false`, so it now reads `AndroidDebugFlag.enabled`, which `Amethyst.onCreate` sets. The app copy is gone. | -| `service/MainThreadChecker.kt` | `checkNotInMainThread` (HiddenUsersState) | **Done 2026-09-28**: `LocalCache.appHost.assertNotMainThread()`, which the app host delegates to the same check. | +| `service/MainThreadChecker.kt` | `checkNotInMainThread` (HiddenUsersState) | **Done 2026-09-28**: `LocalCache.appHost.assertNotMainThread()`. **Deleted 2026-09-29**: a debug-only Android assertion that shared code could no longer rely on; the checker, its call sites and the host hook are gone. | | `service/location/LocationState.kt` | the `LocationResult` type in `geolocationFlow` and the around-me feed | **Done 2026-09-28**: `commons/model/location/LocationResult.kt`; 25 files repointed. | | `service/uploads/FileHeader.kt` | the `FileHeader` data type in three send methods | **Done 2026-09-28**: `commons/service/upload/FileHeader.kt` (with `BlurhashWrapper`/`ThumbhashWrapper` moved from commonsUI, same package); `prepare` stays in the app as `FileHeader.Companion` extensions. | | `service/relayClient/…/BuzzMembershipEoseManager.kt` | the `MembershipNotificationKinds` constant | **Done 2026-09-28**: `commons/model/buzz/BuzzMembershipKinds.kt`. | @@ -356,10 +356,83 @@ assemblers, `EventSync`, …). Packages are renamed on the way: mint screens; relay and relay-group members; communities; Event Sync; the vanish screens; and the feed filter spinner with the feed views they share. `ViewModelProvider.Factory` implementations now override the multiplatform `create(KClass, CreationExtras)`. - - **Next:** re-measure; the exits now in front are the upload/gallery stack, the - Android-only `AccountViewModel` actions (`urlPreview`, media saving), `LoadCityName`, - `ReactionsRow` imports, the remaining `Amethyst.instance` reads (location, favorites, - Tor, browser icons, scheduled posts) and `AppSettingsScreen`. + - **Wave 3 cuts:** + - An `AppServices` port (`commons/service`) holds the app-wide stores screens read: + favorites, browser history and favicons, napplet and signer permissions, Tor + settings. It is installed as `LocalAppServices`; `AccountViewModelHost` gained the + Tor relay evaluation and the NIP-42 auth state. + - `AppPlatform` gained the camera QR scanner, an `AppLauncher` (favorite apps and web + links) and the Concord/napplet/manifest/favicon image models. + - The QR drawer is shared. Encoding sits behind `encodeQrMatrix`: zxing core on + JVM/Android, nothing yet on iOS. `KeepScreenBrightAndAwake` is expect/actual. + - `uriToRoute` left `MainActivity`. `ReactionsRow` and `LoadCityName` are imported + through the note-platform shims. Settings rows left `AppSettingsScreen`. + `formatGrouped` replaced `NumberFormat`. + - **Wave 3 moved:** 15 screens, 47 files. That covers the browser, favorite apps, + connected apps, bottom-bar settings, public chats, relay info, relay-group browse, the + Buzz boards and invite, Concord home, backup conflicts, show-QR and software-app + detail. `AppBottomBar` moved with them. + - **Main-thread checker deleted:** it was a debug-only Android assertion. Once the cache + and feeds were shared, it could only fire through a host hook that is a no-op + everywhere else. + - **Wave 4 cuts:** + - `@SuppressLint` became `@Suppress` app-wide. + - `formatHistoryReachDate` is expect/actual. + - quartz's `ConcurrentSet`/`ConcurrentMap` replaced `java.util.concurrent` in the Buzz + view models. + - Settings painter icons are `DrawableResource`s. + - Share buttons use `rememberTextSharer`. + - `AppPlatform.isCastingAvailable` and `supportsEmbeddedAppTabs` replace + `BuildConfig` and an API-level check. + - More of the synchronous-tier strings now have Compose copies. + - **Wave 4 moved:** 28 screens, 66 files: hashtag, relay and geohash feeds; people lists + and follow packs; the hidden/blocked/muted/spam settings; the Cordn hub, key-package, + link and coordinator screens; relay auth, privacy, home tabs, security filters, profile + UI, drawer and video-player settings; Cashu wallet, NWC and CLINK setup; the Concord + channel list and invite links; the attestation screen and the redirect loader. + - **Wave 5 cuts:** + - `PlatformBackHandler` wraps AndroidX's `BackHandler` on Android and Compose + Multiplatform's `ui-backhandler` on desktop and iOS. + - `rememberLongNotice` joins `rememberShortNotice`. + - The wallet screens use `formatGrouped` and the shared clipboard extensions. + - A shared `formatMonthDayTime` builds its label from `DateSkeletonFormatter` and the + time-of-day formatter. + - `LocalCacheHost.usageCounter` feeds the resource-usage ledger. + - The flavour-specific legal settings section is `AppPlatform.legalSettingsCategory`. + - **Wave 5 moved:** 11 screens, 23 files: the wallet detail, send, receive and + transaction screens; on-chain transactions; the Buzz canvas, agent console and DM + list; badge awarding; old bookmarks; and the settings index. + - **Wave 6 cuts:** + - The gallery picker is shared. `SelectedMedia` holds an `expect abstract class + MediaUri`, which is `actual typealias MediaUri = android.net.Uri` on Android, so the + upload code reading `media.uri` is unchanged. The picker launchers are + expect/actual; desktop and iOS report a cancel for now. + - Other expect/actuals: `JavaSerializable`, `imageContentReceiver` (image paste) and + `SetDialogToEdgeToEdge`. + - The editor's URL highlighting uses quartz's `UrlDetector`. + - `MediaMimeTypes` moved to commons. + - **Wave 6 moved:** the NIP-46 signer screen. The other cuts clear blockers that sit + behind the upload stack. + - **Upload port (2026-09-29):** + - The upload model is in `commons.service.uploads`: `UploadOrchestrator`, + `MultiOrchestrator`, `UploadingState`, `CompressorQuality`, `MediaCompressorResult` + and the `MediaUploader` port. + - Errors are an `UploadError` enum. commonsUI maps it to a string (`errorResource`). + - The Android pipeline (compression, metadata stripping, encryption, the + NIP-95/NIP-96/Blossom uploaders) stays in the app as `AndroidMediaUploader`. It is + reached through `AccountViewModelHost.mediaUploader`. + - Composer view models take a `MediaUploader` instead of a `Context`. + - Five view models had their own copy of strip, compress and upload for a single + image. They now call the shared `uploadToDefaultServer`. + - Moved: the profile editor, the emoji pack screen and the emoji pack metadata screen + (7 files). + - **Next:** most composer screens still exit through `ShowImageUploadItem` (the upload + preview, which uses Android media APIs to show a thumbnail) and `SelectFromFiles`. + After those comes the per-composer state: `ShortNotePostViewModel`, `PreviewState` + and the location pickers. The video player stack (media3) holds about 22 screens. + Smaller exits remain: the Android-only `AccountViewModel` actions, + `CalendarTimeFormat`, `NestUiState`, `NappletLauncher`, `UpdateZapAmountDialog` + (biometrics), the `java.time` pickers and the language list. 7. **Navigation**: the library swap, then `AppNavigation` + rail + drawer + bottom bar. 8. **The app root port** and the new JVM shim. Then the Desktop feature inventory, and retiring the old `desktopApp`. diff --git a/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.android.kt b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.android.kt new file mode 100644 index 0000000000..b38e335f3f --- /dev/null +++ b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.android.kt @@ -0,0 +1,27 @@ +/* + * 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.commons.service.uploads + +import android.net.Uri + +actual typealias MediaUri = Uri + +actual fun MediaUri.lastPathSegmentOrNull(): String? = lastPathSegment diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/CashuWalletDiscovery.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/cashu/CashuWalletDiscovery.kt similarity index 92% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/CashuWalletDiscovery.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/cashu/CashuWalletDiscovery.kt index 71d2e6a214..643dab3a37 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/CashuWalletDiscovery.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/cashu/CashuWalletDiscovery.kt @@ -32,17 +32,17 @@ import com.vitorpamplona.quartz.nip61Nutzaps.info.NutzapInfoEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.sync.Semaphore -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicInteger import kotlin.coroutines.cancellation.CancellationException /** @@ -126,9 +126,9 @@ class CashuWalletDiscovery( // Distinct by event id — the same kind:17375 is found on many relays, // but a user with wallets created by different apps will have several // distinct ids. Keep them all; the wizard sorts/picks the newest. - val wallets = ConcurrentHashMap() - val nutzapInfos = ConcurrentHashMap() - val completed = AtomicInteger(0) + val wallets = MutableStateFlow>(emptyMap()) + val nutzapInfos = MutableStateFlow>(emptyMap()) + val completed = MutableStateFlow(0) _state.value = State.Crawling(0, relays.size) @@ -151,16 +151,16 @@ class CashuWalletDiscovery( onEvent = { event -> when (event) { is CashuWalletEvent -> - if (event.pubKey == pubKey) wallets.putIfAbsent(event.id, event) + if (event.pubKey == pubKey) wallets.update { if (event.id in it) it else it + (event.id to event) } is NutzapInfoEvent -> - if (event.pubKey == pubKey) nutzapInfos.putIfAbsent(event.id, event) + if (event.pubKey == pubKey) nutzapInfos.update { if (event.id in it) it else it + (event.id to event) } else -> Unit } }, onRelayComplete = { // Up to MAX_CONCURRENT_RELAYS relays finish at once: a coroutine preempted // between the increment and the write would move the progress backwards. - val done = completed.incrementAndGet() + val done = completed.updateAndGet { it + 1 } _state.update { current -> if (current is State.Crawling && current.relaysCompleted >= done) current else State.Crawling(done, relays.size) } @@ -170,8 +170,8 @@ class CashuWalletDiscovery( _state.value = State.Done( - walletEvents = wallets.values.sortedByDescending { it.createdAt }, - nutzapInfoEvents = nutzapInfos.values.sortedByDescending { it.createdAt }, + walletEvents = wallets.value.values.sortedByDescending { it.createdAt }, + nutzapInfoEvents = nutzapInfos.value.values.sortedByDescending { it.createdAt }, ) } catch (e: CancellationException) { throw e diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/favorites/FavoriteCoordinate.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/favorites/FavoriteCoordinate.kt new file mode 100644 index 0000000000..1bcfb522b9 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/favorites/FavoriteCoordinate.kt @@ -0,0 +1,39 @@ +/* + * 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.commons.favorites + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent +import com.vitorpamplona.quartz.nip5dNapplets.NamedNappletEvent + +/** + * The addressable coordinate `kind:pubkey:dtag` used to key an nsite/napplet favorite. Stored + * instead of the content hash so the favorite survives routine code/manifest updates. + */ +fun favoriteCoordinateOf(event: Event): String { + val dTag = + when (event) { + is NamedNappletEvent -> event.identifier() + is NamedSiteEvent -> event.identifier() + else -> "" + } + return "${event.kind}:${event.pubKey}:$dTag" +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/ChannelFeedContentState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/ChannelFeedContentState.kt index 296893376c..b08f4a27d2 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/ChannelFeedContentState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/ChannelFeedContentState.kt @@ -25,7 +25,6 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.service.BundledInsert import com.vitorpamplona.amethyst.commons.service.BundledUpdate import com.vitorpamplona.amethyst.commons.util.equalImmutableLists @@ -73,8 +72,6 @@ class ChannelFeedContentState( } fun refreshSuspended() { - LocalCache.appHost.assertNotMainThread() - isRefreshing.value = true try { lastFeedKey = localFilter.feedKey() diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt index 119f2ff33e..056c4e3018 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt @@ -162,8 +162,6 @@ class CachePruner( } fun pruneOldMessages() { - cache.appHost.assertNotMainThread() - cache.ephemeralChannels.forEach { _, channel -> pruneOldMessagesChannel(channel) } @@ -310,8 +308,6 @@ class CachePruner( } fun pruneRepliesAndReactions(accounts: Set) { - cache.appHost.assertNotMainThread() - val toBeRemoved = cache.notes.filter { _, note -> ( @@ -448,8 +444,6 @@ class CachePruner( } fun pruneExpiredEvents() { - cache.appHost.assertNotMainThread() - val now = TimeUtils.now() val versionsToBeRemoved = cache.notes.filter { _, it -> it.event?.isExpirationBefore(now) == true } val addressesToBeRemoved = cache.addressables.filter { _, it -> it.event?.isExpirationBefore(now) == true } @@ -481,8 +475,6 @@ class CachePruner( * authors the reader never muted. */ fun pruneHiddenEvents(hidden: LiveHiddenUsers) { - cache.appHost.assertNotMainThread() - val childrenToBeRemoved = mutableListOf() val toBeRemoved = diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CacheSearch.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CacheSearch.kt index 6abcdaefd3..9537f83e0e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CacheSearch.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CacheSearch.kt @@ -69,8 +69,6 @@ class CacheSearch( ): List { if (username.isBlank()) return emptyList() - cache.appHost.assertNotMainThread() - val key = decodePublicKeyAsHexOrNull(username) if (key != null) { @@ -164,8 +162,6 @@ class CacheSearch( filters: List, hidden: LiveHiddenUsers, ): List { - cache.appHost.assertNotMainThread() - if (filters.isEmpty()) return emptyList() // Distinct across filters: a union of arms (a hashtag asks #t, #l and the comment tags) @@ -201,8 +197,6 @@ class CacheSearch( text: String, hidden: LiveHiddenUsers, ): List { - cache.appHost.assertNotMainThread() - if (text.isBlank()) return emptyList() val key = decodeEventIdAsHexOrNull(text) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt index ee093b66e5..c53f0d9858 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt @@ -3367,8 +3367,6 @@ open class EventCache : var verifyMeter: ((elapsedNanos: Long, valid: Boolean) -> Unit)? = null fun justVerify(event: Event): Boolean { - appHost.assertNotMainThread() - val meter = verifyMeter if (meter == null) return justVerifyInner(event) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt index e004e23b69..f0885e8207 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/LocalCacheHost.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.commons.model.cache import com.vitorpamplona.amethyst.commons.relays.nip11RelayInfo.Nip11CachedRetriever import com.vitorpamplona.amethyst.commons.relays.nip11RelayInfo.Nip11Fetcher +import com.vitorpamplona.amethyst.commons.service.UsageCounter import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -81,6 +82,9 @@ interface LocalCacheHost { */ fun relaySelfPubKey(relay: NormalizedRelayUrl): HexKey? = null + /** The resource-usage ledger's counters; counts nothing by default. */ + val usageCounter: UsageCounter get() = UsageCounter.None + /** * The relay's NIP-11 document as far as the shell has it in memory. Never fetches: `null` * (or a document with nothing in it) means "not loaded yet", which callers must treat as @@ -98,12 +102,6 @@ interface LocalCacheHost { */ fun lnurlEndpoint(lnurlpUrl: String): LnurlEndpointInfo? = null - /** - * Throws if called on the platform's main thread. Signature verification and the cache - * sweeps are far too slow to run there. A no-op by default and on release builds. - */ - fun assertNotMainThread() {} - /** The neutral host: an own IO scope and nothing else. */ companion object Default : LocalCacheHost { override val scope: CoroutineScope by lazy { CoroutineScope(Dispatchers.IO + SupervisorJob()) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/chats/HistoryDateFormat.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/chats/HistoryDateFormat.kt new file mode 100644 index 0000000000..dc617f48ed --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/chats/HistoryDateFormat.kt @@ -0,0 +1,27 @@ +/* + * 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.commons.model.chats + +/** + * Formats a paging reach point (epoch seconds) to a short, locale-aware month-year label, e.g. + * "Jun 2026", for the shared [com.vitorpamplona.amethyst.commons.ui.feeds.DmHistoryLoadingCard]. + */ +expect fun formatHistoryReachDate(epochSeconds: Long): String diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/HiddenUsersState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/HiddenUsersState.kt index 7f50f68545..bad5eaa0a9 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/HiddenUsersState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/HiddenUsersState.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.commons.model.nip51Lists import com.vitorpamplona.amethyst.commons.model.AccountSettings import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers -import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip51Lists.muteList.tags.EventTag import com.vitorpamplona.quartz.nip51Lists.muteList.tags.HashtagTag @@ -84,7 +83,6 @@ class HiddenUsersState( settings.syncedSettings.security.showSensitiveContent, settings.syncedSettings.security.maxHashtagLimit, ) { blockList, muteList, transientHiddenUsers, showSensitiveContent, maxHashtagLimit -> - LocalCache.appHost.assertNotMainThread() emit(assembleLiveHiddenUsers(blockList, muteList, transientHiddenUsers, showSensitiveContent, maxHashtagLimit)) }.onStart { emit( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/notifications/CardFeedContentState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/notifications/CardFeedContentState.kt index 7fdd14bc57..0abaeb2f2d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/notifications/CardFeedContentState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/notifications/CardFeedContentState.kt @@ -123,8 +123,6 @@ class CardFeedContentState( private fun refreshSuspended() = refreshLock.withLock { refreshLocked() } private fun refreshLocked() { - LocalCache.appHost.assertNotMainThread() - try { isRefreshing.value = true @@ -170,8 +168,6 @@ class CardFeedContentState( } private fun convertToCard(notes: Collection): List { - LocalCache.appHost.assertNotMainThread() - val reactionsPerEvent = mutableMapOf>() notes .filter { it.event is ReactionEvent } @@ -544,8 +540,6 @@ class CardFeedContentState( } fun updateFeedWith(newNotes: Set) { - LocalCache.appHost.assertNotMainThread() - if (localFilter is AdditiveFeedFilter && _feedContent.value is CardFeedState.Loaded) { invalidateInsertData(newNotes) } else { diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/podcasts/Rfc2822Date.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/podcasts/Rfc2822Date.kt new file mode 100644 index 0000000000..86ea1ca0b0 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/podcasts/Rfc2822Date.kt @@ -0,0 +1,52 @@ +/* + * 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.commons.podcasts + +private val WEEKDAYS = arrayOf("Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed") +private val MONTHS = arrayOf("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec") + +/** + * [epochSeconds] as an RFC 2822 date in GMT (`Tue, 24 Jun 2025 12:00:00 GMT`), the form + * Podcasting 2.0's `pubdate` and RSS use. Matches `java.time`'s `RFC_1123_DATE_TIME`, which + * does not zero-pad the day. + */ +fun rfc2822Date(epochSeconds: Long): String { + val days = epochSeconds.floorDiv(86_400L) + val secondOfDay = epochSeconds.mod(86_400L) + + // Civil-from-days (Howard Hinnant), proleptic Gregorian. + val z = days + 719_468L + val era = z.floorDiv(146_097L) + val doe = z - era * 146_097L + val yoe = (doe - doe / 1_460L + doe / 36_524L - doe / 146_096L) / 365L + val doy = doe - (365L * yoe + yoe / 4L - yoe / 100L) + val mp = (5L * doy + 2L) / 153L + val day = doy - (153L * mp + 2L) / 5L + 1L + val month = if (mp < 10L) mp + 3L else mp - 9L + val year = yoe + era * 400L + if (month <= 2L) 1L else 0L + + val weekday = WEEKDAYS[days.mod(7L).toInt()] + val hh = (secondOfDay / 3_600L).toString().padStart(2, '0') + val mm = (secondOfDay / 60L % 60L).toString().padStart(2, '0') + val ss = (secondOfDay % 60L).toString().padStart(2, '0') + + return "$weekday, $day ${MONTHS[(month - 1L).toInt()]} $year $hh:$mm:$ss GMT" +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt new file mode 100644 index 0000000000..37ca48c1a4 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/AppServices.kt @@ -0,0 +1,56 @@ +/* + * 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.commons.service + +import com.vitorpamplona.amethyst.commons.browser.BrowserHistoryRegistry +import com.vitorpamplona.amethyst.commons.browser.BrowserIconRegistry +import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionStore +import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry +import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger +import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow +import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver + +/** + * App-wide stores that outlive any one account and that screens read directly: the favorites + * row, the browser's history and favicons, what connected apps may do, the Tor settings and the + * Namecoin resolver. + * Account-scoped services reach screens through the account's ViewModel instead. + * + * A front end implements it over its own modules; shared UI reads it through `LocalAppServices`. + */ +interface AppServices { + val favoriteApps: FavoriteAppsRegistry + + val browserHistory: BrowserHistoryRegistry + + val browserIcons: BrowserIconRegistry + + /** What each napplet/nsite has been granted, per account. */ + val nappletPermissionLedger: NappletPermissionLedger + + /** What each remote-signer client has been granted. */ + val signerPermissionStore: NostrSignerPermissionStore + + val torSettings: TorSettingsFlow + + /** Resolves `.bit` names and `d/`/`id/` identifiers over the configured ElectrumX servers. */ + val namecoinResolver: NamecoinNameResolver +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/MainThreadChecker.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/UsageCounter.kt similarity index 73% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/MainThreadChecker.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/UsageCounter.kt index 229a23f3db..5e6118e8d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/MainThreadChecker.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/UsageCounter.kt @@ -18,19 +18,17 @@ * 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.service +package com.vitorpamplona.amethyst.commons.service -import android.os.Looper -import com.vitorpamplona.amethyst.BuildConfig +/** Adds to a diagnostics counter in the app's resource-usage ledger. Never throws. */ +fun interface UsageCounter { + fun add( + key: String, + amount: Long, + ) -fun checkNotInMainThread() { - if (BuildConfig.DEBUG && isMainThread()) { - throw OnMainThreadException("It should not be in the MainThread") + companion object { + /** Counts nothing: hosts without a ledger. */ + val None = UsageCounter { _, _ -> } } } - -fun isMainThread() = Looper.myLooper() == Looper.getMainLooper() - -class OnMainThreadException( - str: String, -) : RuntimeException(str) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/CompressorQuality.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/CompressorQuality.kt new file mode 100644 index 0000000000..246f907736 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/CompressorQuality.kt @@ -0,0 +1,43 @@ +/* + * 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.commons.service.uploads + +enum class CompressorQuality { + VERY_LOW, + LOW, + MEDIUM, + HIGH, + VERY_HIGH, + UNCOMPRESSED, + ; + + companion object { + /** The media-quality slider's position (0 low … 3 uncompressed) as a quality. */ + fun fromSlider(position: Int): CompressorQuality = + when (position) { + 0 -> LOW + 1 -> MEDIUM + 2 -> HIGH + 3 -> UNCOMPRESSED + else -> MEDIUM + } + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaCompressorResult.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaCompressorResult.kt new file mode 100644 index 0000000000..e1d63e4e8b --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaCompressorResult.kt @@ -0,0 +1,28 @@ +/* + * 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.commons.service.uploads + +/** The file to upload after compression: the original when nothing was compressed. */ +class MediaCompressorResult( + val uri: MediaUri, + val contentType: String?, + val size: Long?, +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaMimeTypes.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaMimeTypes.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaMimeTypes.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaMimeTypes.kt index 7de127608b..c6e553602d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaMimeTypes.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaMimeTypes.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.service.uploads +package com.vitorpamplona.amethyst.commons.service.uploads // RFC 9081 defines image/avif for both still and animated AVIF. // image/avif-sequence is NOT IANA-registered and is intentionally not handled. diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUploader.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUploader.kt new file mode 100644 index 0000000000..a20a1de7bc --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUploader.kt @@ -0,0 +1,124 @@ +/* + * 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.commons.service.uploads + +import com.vitorpamplona.amethyst.commons.model.Account +import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.utils.ciphers.NostrCipher + +/** + * The platform's media pipeline: compresses, strips metadata from, optionally encrypts, and + * uploads a picked file to a NIP-95, NIP-96 or Blossom server, reporting each stage on + * [progress]. Reached through `AccountViewModelHost.mediaUploader`; [UploadOrchestrator] and + * [MultiOrchestrator] are the shared front ends composers use. + */ +interface MediaUploader { + suspend fun upload( + progress: UploadOrchestrator, + uri: MediaUri, + mimeType: String?, + alt: String?, + contentWarningReason: String?, + compressionQuality: CompressorQuality, + server: ServerName, + account: Account, + useH265: Boolean, + stripMetadata: Boolean, + onStrippingFailed: suspend () -> Boolean, + convertGifToMp4: Boolean, + forcedSigner: NostrSigner?, + ): UploadingState.UploadingFinalState + + suspend fun uploadEncrypted( + progress: UploadOrchestrator, + uri: MediaUri, + mimeType: String?, + alt: String?, + contentWarningReason: String?, + compressionQuality: CompressorQuality, + encrypt: NostrCipher, + server: ServerName, + account: Account, + useH265: Boolean, + stripMetadata: Boolean, + onStrippingFailed: suspend () -> Boolean, + convertGifToMp4: Boolean, + forcedSigner: NostrSigner?, + ): UploadingState.UploadingFinalState + + /** Compresses [uri] when [compressionQuality] or [convertGifToMp4] asks for it, else returns it as is. */ + suspend fun compressIfNeeded( + progress: UploadOrchestrator, + uri: MediaUri, + mimeType: String?, + compressionQuality: CompressorQuality, + useH265: Boolean, + convertGifToMp4: Boolean, + ): MediaCompressorResult +} + +/** No media pipeline on this front end yet: every upload fails with a clear message. */ +object UnavailableMediaUploader : MediaUploader { + private const val REASON = "Uploads are not available on this platform yet" + + override suspend fun upload( + progress: UploadOrchestrator, + uri: MediaUri, + mimeType: String?, + alt: String?, + contentWarningReason: String?, + compressionQuality: CompressorQuality, + server: ServerName, + account: Account, + useH265: Boolean, + stripMetadata: Boolean, + onStrippingFailed: suspend () -> Boolean, + convertGifToMp4: Boolean, + forcedSigner: NostrSigner?, + ): UploadingState.UploadingFinalState = progress.error(UploadError.FAILED_TO_UPLOAD_MEDIA, REASON) + + override suspend fun uploadEncrypted( + progress: UploadOrchestrator, + uri: MediaUri, + mimeType: String?, + alt: String?, + contentWarningReason: String?, + compressionQuality: CompressorQuality, + encrypt: NostrCipher, + server: ServerName, + account: Account, + useH265: Boolean, + stripMetadata: Boolean, + onStrippingFailed: suspend () -> Boolean, + convertGifToMp4: Boolean, + forcedSigner: NostrSigner?, + ): UploadingState.UploadingFinalState = progress.error(UploadError.FAILED_TO_UPLOAD_MEDIA, REASON) + + override suspend fun compressIfNeeded( + progress: UploadOrchestrator, + uri: MediaUri, + mimeType: String?, + compressionQuality: CompressorQuality, + useH265: Boolean, + convertGifToMp4: Boolean, + ): MediaCompressorResult = MediaCompressorResult(uri, mimeType, null) +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.kt new file mode 100644 index 0000000000..b4de3123e9 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.kt @@ -0,0 +1,78 @@ +/* + * 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.commons.service.uploads + +/** + * A picked file's address as the platform hands it over: Android's `content://` [android.net.Uri] + * (a typealias there, so app code keeps reading it as a `Uri`), a string-backed subclass + * elsewhere. + */ +expect abstract class MediaUri { + /** The address as a string (`content://…` on Android). */ + abstract override fun toString(): String +} + +/** The last path segment of this address, if it has one (on Android, `Uri.lastPathSegment`). */ +expect fun MediaUri.lastPathSegmentOrNull(): String? + +/** + * The last path segment of a string address, percent-decoded, or null when it has no path + * (`content://host`, `https://host/`). Accepts backslash separators, so a Windows path works too. The + * string-backed platforms use it to match Android's `Uri.lastPathSegment`. + */ +internal fun lastPathSegmentOf(address: String): String? { + var path = address.substringBefore('#').substringBefore('?') + val schemeEnd = path.indexOf("://") + if (schemeEnd >= 0) { + val pathStart = path.indexOf('/', schemeEnd + 3) + if (pathStart < 0) return null + path = path.substring(pathStart) + } + val segment = path.replace('\\', '/').trimEnd('/').substringAfterLast('/') + return if (segment.isEmpty()) null else percentDecodeUtf8(segment) +} + +/** Decodes `%XX` escapes as UTF-8 bytes; malformed escapes are kept as written. */ +private fun percentDecodeUtf8(input: String): String { + if ('%' !in input) return input + val out = StringBuilder(input.length) + val bytes = ArrayList() + var i = 0 + while (i < input.length) { + val c = input[i] + val hex = if (c == '%' && i + 2 < input.length && isHex(input[i + 1]) && isHex(input[i + 2])) input.substring(i + 1, i + 3).toInt(16) else null + if (hex != null) { + bytes.add(hex.toByte()) + i += 3 + } else { + if (bytes.isNotEmpty()) { + out.append(bytes.toByteArray().decodeToString()) + bytes.clear() + } + out.append(c) + i++ + } + } + if (bytes.isNotEmpty()) out.append(bytes.toByteArray().decodeToString()) + return out.toString() +} + +private fun isHex(c: Char) = c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F' diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MultiOrchestrator.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MultiOrchestrator.kt index 797688b86c..7958b58003 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MultiOrchestrator.kt @@ -18,20 +18,18 @@ * 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.service.uploads +package com.vitorpamplona.amethyst.commons.service.uploads -import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.utils.ciphers.NostrCipher import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch @@ -67,7 +65,7 @@ class MultiOrchestrator( mediaQuality: CompressorQuality, server: ServerName, account: Account, - context: Context, + uploader: MediaUploader, useH265: Boolean = false, stripMetadata: Boolean = true, onStrippingFailed: suspend () -> Boolean = { true }, @@ -86,7 +84,7 @@ class MultiOrchestrator( mediaQuality, server, account, - context, + uploader, useH265, stripMetadata, onStrippingFailed, @@ -109,7 +107,7 @@ class MultiOrchestrator( cipher: NostrCipher, server: ServerName, account: Account, - context: Context, + uploader: MediaUploader, useH265: Boolean = false, stripMetadata: Boolean = true, onStrippingFailed: suspend () -> Boolean = { true }, @@ -129,7 +127,7 @@ class MultiOrchestrator( cipher, server, account, - context, + uploader, useH265, stripMetadata, onStrippingFailed, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/SelectedMedia.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/SelectedMedia.kt new file mode 100644 index 0000000000..195105ebf1 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/SelectedMedia.kt @@ -0,0 +1,65 @@ +/* + * 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.commons.service.uploads + +import androidx.compose.runtime.Stable + +@Stable +class SelectedMedia( + val uri: MediaUri, + val mimeType: String?, +) { + fun isImage() = mimeType?.startsWith("image") + + fun isGif() = mimeType?.equals("image/gif", ignoreCase = true) == true + + fun isVideo() = mimeType?.startsWith("video") + + fun isAudio() = mimeType?.startsWith("audio") + + fun isNotMedia() = + mimeType?.let { + !(it.startsWith("image") || it.startsWith("video") || it.startsWith("audio")) + } ?: true + + fun isDocument() = mimeType == "application/pdf" + + /** + * Returns true if [MediaCompressor.compress] would actually compress this file when a + * non-UNCOMPRESSED quality is selected. AVIF, GIF, SVG, and unknown MIME types pass + * through MediaCompressor unchanged — the compression-quality slider has no effect on + * them, so the UI hides the slider when no selected files are compressible. + * + * Keep this in sync with the branching in MediaCompressor.compress() — if either side + * drifts, the UI will lie to the user. + */ + fun isCompressible(): Boolean { + val mt = mimeType?.lowercase() ?: return false + return when { + mt.startsWith("video") -> true + mt.startsWith("image") -> + !mt.contains("gif") && + !mt.contains("svg") && + !isAvif(mt) + else -> false + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectedMediaProcessing.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/SelectedMediaProcessing.kt similarity index 90% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectedMediaProcessing.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/SelectedMediaProcessing.kt index 3651631454..c07ae92cfa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectedMediaProcessing.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/SelectedMediaProcessing.kt @@ -18,9 +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.actions.uploads - -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator +package com.vitorpamplona.amethyst.commons.service.uploads class SelectedMediaProcessing( val media: SelectedMedia, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/UploadError.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/UploadError.kt new file mode 100644 index 0000000000..48168136b1 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/UploadError.kt @@ -0,0 +1,35 @@ +/* + * 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.commons.service.uploads + +/** Why an upload failed. The UI names it with `UploadingState.Error.errorResource`. */ +enum class UploadError { + MEDIA_TOO_BIG_FOR_NIP95, + COULD_NOT_CHECK_DOWNLOADED_FILE, + COULD_NOT_OPEN_COMPRESSED_FILE, + LOGIN_WITH_PRIVATE_KEY, + FAILED_TO_UPLOAD_MEDIA, + BLOSSOM_PAYMENT_REQUIRED, + SERVER_DID_NOT_PROVIDE_URL, + COULD_NOT_DOWNLOAD_FROM_SERVER, + AVIF_METADATA_STRIP_FAILED, + UPLOAD_CANCELLED, +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/UploadOrchestrator.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/UploadOrchestrator.kt new file mode 100644 index 0000000000..dfd672ce8b --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/UploadOrchestrator.kt @@ -0,0 +1,121 @@ +/* + * 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.commons.service.uploads + +import com.vitorpamplona.amethyst.commons.model.Account +import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName +import com.vitorpamplona.amethyst.commons.service.upload.FileHeader +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.utils.ciphers.NostrCipher +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map + +/** + * One file's upload, as the composer sees it: its progress and final state. The work itself is + * the [MediaUploader]'s, which reports each stage back through [updateState]. + */ +class UploadOrchestrator { + val progress = MutableStateFlow(0.0) + val progressState = MutableStateFlow(UploadingState.Ready) + + val isUploading = + progressState.map { + progressState.value !is UploadingState.Ready && progressState.value !is UploadingState.Error && progressState.value !is UploadingState.Finished + } + + fun error( + error: UploadError, + vararg params: String, + ) = UploadingState.Error(error, params).also { updateState(0.0, it) } + + fun finish(result: OrchestratorResult) = + UploadingState + .Finished(result) + .also { updateState(1.0, it) } + + fun updateState( + newProgress: Double, + newState: UploadingState, + ) { + progress.value = newProgress + progressState.value = newState + } + + suspend fun compressIfNeeded( + uri: MediaUri, + mimeType: String?, + compressionQuality: CompressorQuality, + uploader: MediaUploader, + useH265: Boolean = false, + convertGifToMp4: Boolean = false, + ): MediaCompressorResult = uploader.compressIfNeeded(this, uri, mimeType, compressionQuality, useH265, convertGifToMp4) + + suspend fun upload( + uri: MediaUri, + mimeType: String?, + alt: String?, + contentWarningReason: String?, + compressionQuality: CompressorQuality, + server: ServerName, + account: Account, + uploader: MediaUploader, + useH265: Boolean = false, + stripMetadata: Boolean = true, + onStrippingFailed: suspend () -> Boolean = { true }, + convertGifToMp4: Boolean = false, + forcedSigner: NostrSigner? = null, + ): UploadingState.UploadingFinalState = uploader.upload(this, uri, mimeType, alt, contentWarningReason, compressionQuality, server, account, useH265, stripMetadata, onStrippingFailed, convertGifToMp4, forcedSigner) + + suspend fun uploadEncrypted( + uri: MediaUri, + mimeType: String?, + alt: String?, + contentWarningReason: String?, + compressionQuality: CompressorQuality, + encrypt: NostrCipher, + server: ServerName, + account: Account, + uploader: MediaUploader, + useH265: Boolean = false, + stripMetadata: Boolean = true, + onStrippingFailed: suspend () -> Boolean = { true }, + convertGifToMp4: Boolean = false, + forcedSigner: NostrSigner? = null, + ): UploadingState.UploadingFinalState = uploader.uploadEncrypted(this, uri, mimeType, alt, contentWarningReason, compressionQuality, encrypt, server, account, useH265, stripMetadata, onStrippingFailed, convertGifToMp4, forcedSigner) + + sealed class OrchestratorResult { + class NIP95Result( + val fileHeader: FileHeader, + val bytes: ByteArray, + val mimeTypeBeforeEncryption: String?, + val hashBeforeEncryption: String?, + ) : OrchestratorResult() + + class ServerResult( + val fileHeader: FileHeader, + val url: String, + val magnet: String?, + val uploadedHash: String?, + val mimeTypeBeforeEncryption: String?, + val hashBeforeEncryption: String?, + ) : OrchestratorResult() + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/UploadingState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/UploadingState.kt new file mode 100644 index 0000000000..0c861368ae --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/UploadingState.kt @@ -0,0 +1,46 @@ +/* + * 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.commons.service.uploads + +sealed class UploadingState { + data object Ready : UploadingState() + + data object Compressing : UploadingState() + + data object Uploading : UploadingState() + + data object ServerProcessing : UploadingState() + + data object Downloading : UploadingState() + + data object Hashing : UploadingState() + + sealed class UploadingFinalState : UploadingState() + + class Finished( + val result: UploadOrchestrator.OrchestratorResult, + ) : UploadingFinalState() + + class Error( + val error: UploadError, + val params: Array, + ) : UploadingFinalState() +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.kt new file mode 100644 index 0000000000..89e3f1a75e --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.kt @@ -0,0 +1,24 @@ +/* + * 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.commons.util + +/** [value] with the locale's digit grouping: `1,234,567` in English, `1.234.567` in German. */ +expect fun formatGrouped(value: Long): String diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/JavaSerializable.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/JavaSerializable.kt new file mode 100644 index 0000000000..c0fd6badf2 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/JavaSerializable.kt @@ -0,0 +1,27 @@ +/* + * 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.commons.util + +/** + * `java.io.Serializable` where there is a JVM, a marker elsewhere. For values Android must store in + * a Bundle (saveable state, lazy-list keys) that shared code declares. + */ +expect interface JavaSerializable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt index d596783a8e..8f497e176e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelHost.kt @@ -22,11 +22,17 @@ package com.vitorpamplona.amethyst.commons.viewmodels import com.vitorpamplona.amethyst.commons.service.lnurl.LnurlHttpTransport import com.vitorpamplona.amethyst.commons.service.pow.PoWJobFailure +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader import com.vitorpamplona.amethyst.commons.tor.MoneyOpRelayRouting +import com.vitorpamplona.amethyst.commons.tor.TorRelayEvaluation import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.auth.RelayAuthSnapshot import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder +import kotlinx.collections.immutable.PersistentMap import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow /** * What the account's ViewModel needs from the process it runs in: app-wide services that outlive @@ -55,6 +61,15 @@ interface AccountViewModelHost { /** Routes a payment's relays under the money-operations Tor preference while it runs. */ val moneyOpRelays: MoneyOpRelayRouting + /** Which relays go through Tor under the user's settings, kept current. */ + val torRelayEvaluation: StateFlow + + /** NIP-42 authentication state per relay, across every logged-in account. */ + val relayAuthState: StateFlow> + + /** Compresses, strips, encrypts and uploads picked media for the composers. */ + val mediaUploader: MediaUploader + /** Public keys of every account saved on this device. */ val savedAccounts: Flow> diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/chats/HistoryDateFormat.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/chats/HistoryDateFormat.ios.kt new file mode 100644 index 0000000000..63f4f1f0c8 --- /dev/null +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/model/chats/HistoryDateFormat.ios.kt @@ -0,0 +1,32 @@ +/* + * 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.commons.model.chats + +import platform.Foundation.NSDate +import platform.Foundation.NSDateFormatter +import platform.Foundation.dateWithTimeIntervalSince1970 + +// A formatter per call: this runs once per history-card render, and NSDateFormatter is not +// safe to share across threads. +actual fun formatHistoryReachDate(epochSeconds: Long): String = + NSDateFormatter() + .apply { setLocalizedDateFormatFromTemplate("MMMyyyy") } + .stringFromDate(NSDate.dateWithTimeIntervalSince1970(epochSeconds.toDouble())) diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.ios.kt new file mode 100644 index 0000000000..a8e33c608e --- /dev/null +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.ios.kt @@ -0,0 +1,40 @@ +/* + * 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.commons.service.uploads + +actual abstract class MediaUri { + actual abstract override fun toString(): String +} + +/** A picked file addressed by a path or URL string. */ +class StringMediaUri( + val value: String, +) : MediaUri() { + override fun toString(): String = value + + // Value equality, like android.net.Uri: the upload pipeline tells a temp file from the + // user's own by comparing addresses, and deletes the temp one. + override fun equals(other: Any?): Boolean = other is StringMediaUri && other.value == value + + override fun hashCode(): Int = value.hashCode() +} + +actual fun MediaUri.lastPathSegmentOrNull(): String? = lastPathSegmentOf(toString()) diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.ios.kt new file mode 100644 index 0000000000..22e852b588 --- /dev/null +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.ios.kt @@ -0,0 +1,31 @@ +/* + * 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.commons.util + +import platform.Foundation.NSNumber +import platform.Foundation.NSNumberFormatter +import platform.Foundation.NSNumberFormatterDecimalStyle +import platform.Foundation.numberWithLongLong + +actual fun formatGrouped(value: Long): String { + val formatter = NSNumberFormatter().apply { numberStyle = NSNumberFormatterDecimalStyle } + return formatter.stringFromNumber(NSNumber.numberWithLongLong(value)) ?: value.toString() +} diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/JavaSerializable.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/JavaSerializable.ios.kt new file mode 100644 index 0000000000..32f98452cd --- /dev/null +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/JavaSerializable.ios.kt @@ -0,0 +1,23 @@ +/* + * 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.commons.util + +actual interface JavaSerializable diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/chats/HistoryDateFormat.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/chats/HistoryDateFormat.jvmAndroid.kt similarity index 74% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/chats/HistoryDateFormat.kt rename to commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/chats/HistoryDateFormat.jvmAndroid.kt index cf3fd25fb0..a18ee86748 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/chats/HistoryDateFormat.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/chats/HistoryDateFormat.jvmAndroid.kt @@ -24,9 +24,4 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -/** - * The Android locale date formatter for the shared [com.vitorpamplona.amethyst.commons.ui.feeds.DmHistoryLoadingCard] - * — passed in so the shared (KMP) card carries no `java.time` dependency. Formats a paging reach point - * (epoch seconds) to a short month-year label, e.g. "Jun 2026". - */ -fun formatHistoryReachDate(epochSeconds: Long): String = SimpleDateFormat("MMM yyyy", Locale.getDefault()).format(Date(epochSeconds * 1000)) +actual fun formatHistoryReachDate(epochSeconds: Long): String = SimpleDateFormat("MMM yyyy", Locale.getDefault()).format(Date(epochSeconds * 1000)) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.jvmAndroid.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.jvmAndroid.kt new file mode 100644 index 0000000000..b2e00ab2b1 --- /dev/null +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/GroupedNumber.jvmAndroid.kt @@ -0,0 +1,25 @@ +/* + * 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.commons.util + +import java.text.NumberFormat + +actual fun formatGrouped(value: Long): String = NumberFormat.getNumberInstance().format(value) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/JavaSerializable.jvmAndroid.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/JavaSerializable.jvmAndroid.kt new file mode 100644 index 0000000000..580775f5e5 --- /dev/null +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/JavaSerializable.jvmAndroid.kt @@ -0,0 +1,23 @@ +/* + * 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.commons.util + +actual typealias JavaSerializable = java.io.Serializable diff --git a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.jvm.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.jvm.kt new file mode 100644 index 0000000000..a8e33c608e --- /dev/null +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUri.jvm.kt @@ -0,0 +1,40 @@ +/* + * 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.commons.service.uploads + +actual abstract class MediaUri { + actual abstract override fun toString(): String +} + +/** A picked file addressed by a path or URL string. */ +class StringMediaUri( + val value: String, +) : MediaUri() { + override fun toString(): String = value + + // Value equality, like android.net.Uri: the upload pipeline tells a temp file from the + // user's own by comparing addresses, and deletes the temp one. + override fun equals(other: Any?): Boolean = other is StringMediaUri && other.value == value + + override fun hashCode(): Int = value.hashCode() +} + +actual fun MediaUri.lastPathSegmentOrNull(): String? = lastPathSegmentOf(toString()) diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/podcasts/Rfc2822DateTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/podcasts/Rfc2822DateTest.kt new file mode 100644 index 0000000000..8e1eff305a --- /dev/null +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/podcasts/Rfc2822DateTest.kt @@ -0,0 +1,44 @@ +/* + * 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.commons.podcasts + +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import kotlin.test.Test +import kotlin.test.assertEquals + +class Rfc2822DateTest { + private fun javaTime(epochSeconds: Long) = DateTimeFormatter.RFC_1123_DATE_TIME.format(Instant.ofEpochSecond(epochSeconds).atZone(ZoneId.of("GMT"))) + + @Test + fun knownDate() { + assertEquals("Tue, 24 Jun 2025 12:00:00 GMT", rfc2822Date(1_750_766_400L)) + } + + @Test + fun matchesJavaTimeAcrossDates() { + val samples = + listOf(0L, 59L, 86_399L, 86_400L, 951_782_400L, 951_868_799L, 1_709_164_800L, 4_107_542_399L, 1_750_766_400L) + + (0 until 2_000).map { it * 2_629_743L + it * 7_919L } + samples.forEach { assertEquals(javaTime(it), rfc2822Date(it), "epoch $it") } + } +} diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUriTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUriTest.kt new file mode 100644 index 0000000000..752605bc66 --- /dev/null +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/service/uploads/MediaUriTest.kt @@ -0,0 +1,52 @@ +/* + * 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.commons.service.uploads + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNull + +class MediaUriTest { + @Test + fun lastSegmentOfPathsAndUrls() { + assertEquals("song.mp3", StringMediaUri("/home/me/music/song.mp3").lastPathSegmentOrNull()) + assertEquals("My Song.mp3", StringMediaUri("file:///home/me/My%20Song.mp3").lastPathSegmentOrNull()) + assertEquals("café.png", StringMediaUri("https://host/a/caf%C3%A9.png?x=1#y").lastPathSegmentOrNull()) + assertEquals("clip.mp4", StringMediaUri("C:\\Users\\me\\clip.mp4").lastPathSegmentOrNull()) + assertEquals("dir", StringMediaUri("https://host/dir/").lastPathSegmentOrNull()) + assertEquals("100%", StringMediaUri("/tmp/100%").lastPathSegmentOrNull()) + } + + @Test + fun noPathIsNull() { + assertNull(StringMediaUri("content://host").lastPathSegmentOrNull()) + assertNull(StringMediaUri("https://host/").lastPathSegmentOrNull()) + assertNull(StringMediaUri("").lastPathSegmentOrNull()) + } + + @Test + fun comparesByValue() { + assertEquals(StringMediaUri("/a/b.png"), StringMediaUri("/a/b.png")) + assertEquals(StringMediaUri("/a/b.png").hashCode(), StringMediaUri("/a/b.png").hashCode()) + assertNotEquals(StringMediaUri("/a/b.png"), StringMediaUri("/a/c.png")) + } +} diff --git a/commonsUI/build.gradle.kts b/commonsUI/build.gradle.kts index 5073bc8d30..53b1c20463 100644 --- a/commonsUI/build.gradle.kts +++ b/commonsUI/build.gradle.kts @@ -189,6 +189,10 @@ kotlin { // org.jetbrains.androidx.lifecycle variant or accept a // platform-specific ViewModel access pattern on iOS. implementation(libs.androidx.lifecycle.viewmodel.compose) + + // ZXing core encodes the QR codes the shared QrCodeDrawer shows. Pure Java, + // Apache-2.0, and already linked by both apps. + implementation(libs.zxing) } } @@ -208,6 +212,8 @@ kotlin { implementation(libs.androidx.ui.tooling.preview) // WindowInsetsControllerCompat, for DisappearingScaffold's immersive status bar. implementation(libs.androidx.core.ktx) + // BackHandler, behind the shared PlatformBackHandler. Apache-2.0, already in the app. + implementation(libs.androidx.activity.compose) } } @@ -225,6 +231,11 @@ kotlin { val skikoMain = create("skikoMain") { dependsOn(commonMain.get()) + dependencies { + // BackHandler for desktop (Esc) and iOS (swipe), behind PlatformBackHandler. + // JetBrains Compose, Apache-2.0; Android uses AndroidX's instead. + implementation(libs.jetbrains.compose.ui.backhandler) + } } getByName("jvmMain").dependsOn(skikoMain) iosMain.dependsOn(skikoMain) diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/FileSelect.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/FileSelect.android.kt new file mode 100644 index 0000000000..6c87ff3900 --- /dev/null +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/FileSelect.android.kt @@ -0,0 +1,97 @@ +/* + * 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.commons.ui.actions.uploads + +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +@Composable +actual fun FileSelect(onFilesSelected: (ImmutableList) -> Unit) { + // Saveable: a picker open across an activity recreation must not be launched a second time. + var hasLaunched by rememberSaveable { mutableStateOf(false) } + val resolver = LocalContext.current.contentResolver + + val launcher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenMultipleDocuments(), + onResult = { uris: List -> + onFilesSelected( + uris + .map { + SelectedMedia(it, resolver.getType(it)) + }.toImmutableList(), + ) + hasLaunched = false + }, + ) + + @Composable + fun LaunchFilePicker() { + SideEffect { + if (!hasLaunched) { + hasLaunched = true + launcher.launch( + arrayOf( + "audio/*", + "application/pdf", + ), + ) + } + } + } + + LaunchFilePicker() +} + +@Composable +actual fun DocumentSelectSingle( + mimeTypes: List, + onPicked: (SelectedMedia?) -> Unit, +) { + var hasLaunched by rememberSaveable { mutableStateOf(false) } + val resolver = LocalContext.current.contentResolver + val launcher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument(), + onResult = { uri: Uri? -> + hasLaunched = false + onPicked(uri?.let { SelectedMedia(it, resolver.getType(it)) }) + }, + ) + LaunchedEffect(Unit) { + if (!hasLaunched) { + hasLaunched = true + launcher.launch(mimeTypes.toTypedArray()) + } + } +} diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/GallerySelect.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/GallerySelect.android.kt new file mode 100644 index 0000000000..28ea8b58d4 --- /dev/null +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/GallerySelect.android.kt @@ -0,0 +1,105 @@ +/* + * 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.commons.ui.actions.uploads + +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +@Composable +actual fun GallerySelect(onImageUri: (ImmutableList) -> Unit) { + // Saveable: a picker open across an activity recreation must not be launched a second time. + var hasLaunched by rememberSaveable { mutableStateOf(false) } + val resolver = LocalContext.current.contentResolver + + val launcher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickMultipleVisualMedia(10), + onResult = { uris: List -> + onImageUri( + uris + .map { + SelectedMedia(it, resolver.getType(it)) + }.toImmutableList(), + ) + hasLaunched = false + }, + ) + + @Composable + fun LaunchGallery() { + SideEffect { + if (!hasLaunched) { + hasLaunched = true + launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo)) + } + } + } + + LaunchGallery() +} + +@Composable +actual fun GallerySelectSingle( + imagesOnly: Boolean, + onImageUri: (SelectedMedia?) -> Unit, +) { + // Saveable: a picker open across an activity recreation must not be launched a second time. + var hasLaunched by rememberSaveable { mutableStateOf(false) } + val resolver = LocalContext.current.contentResolver + + val launcher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickVisualMedia(), + onResult = { uri: Uri? -> + if (uri != null) { + onImageUri(SelectedMedia(uri, resolver.getType(uri))) + } else { + onImageUri(null) + } + + hasLaunched = false + }, + ) + + @Composable + fun LaunchGallery() { + SideEffect { + if (!hasLaunched) { + hasLaunched = true + launcher.launch(PickVisualMediaRequest(if (imagesOnly) ActivityResultContracts.PickVisualMedia.ImageOnly else ActivityResultContracts.PickVisualMedia.ImageAndVideo)) + } + } + } + + LaunchGallery() +} diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VideoThumbnail.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VideoThumbnail.android.kt new file mode 100644 index 0000000000..a746baa6a4 --- /dev/null +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VideoThumbnail.android.kt @@ -0,0 +1,82 @@ +/* + * 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.commons.ui.actions.uploads + +import android.content.Context +import android.graphics.Bitmap +import android.media.MediaMetadataRetriever +import android.net.Uri +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.quartz.utils.Log +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Composable +actual fun rememberVideoThumbnail(media: SelectedMedia): ImageBitmap? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return null + val context = LocalContext.current + val thumb by produceState(null, media) { + value = + withContext(Dispatchers.IO) { + try { + createVideoThumb(context, media.uri)?.asImageBitmap() + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.w("VideoThumbnail", "Couldn't create thumbnail, but the video can be uploaded", e) + null + } + } + } + return thumb +} + +// A preview tile: a frame this size is plenty, where a full 4K frame would be ~33 MB. +private const val THUMB_MAX_SIDE = 1024 + +/** + * Creates a bitmap thumbnail, at most [THUMB_MAX_SIDE] on its longer side, from a video uri of the + * scheme type content:// + */ +@RequiresApi(Build.VERSION_CODES.O_MR1) +private fun createVideoThumb( + context: Context, + uri: Uri, +): Bitmap? { + val retriever = MediaMetadataRetriever() + return try { + retriever.setDataSource(context, uri) + retriever.getScaledFrameAtTime(-1, MediaMetadataRetriever.OPTION_CLOSEST_SYNC, THUMB_MAX_SIDE, THUMB_MAX_SIDE) + } catch (ex: Exception) { + Log.w("VideoThumbnail", "Couldn't create thumbnail, but the video can be uploaded", ex) + null + } finally { + retriever.release() + } +} diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.android.kt new file mode 100644 index 0000000000..20af3c3130 --- /dev/null +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.android.kt @@ -0,0 +1,58 @@ +/* + * 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.commons.ui.components + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.content.MediaType +import androidx.compose.foundation.content.ReceiveContentListener +import androidx.compose.foundation.content.TransferableContent +import androidx.compose.foundation.content.consume +import androidx.compose.foundation.content.contentReceiver +import androidx.compose.foundation.content.hasMediaType +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri + +@OptIn(ExperimentalFoundationApi::class) +actual fun Modifier.imageContentReceiver(onImage: (MediaUri, String?) -> Unit): Modifier = + contentReceiver( + object : ReceiveContentListener { + override fun onReceive(transferableContent: TransferableContent): TransferableContent? { + if (!transferableContent.hasMediaType(MediaType.Image)) { + return transferableContent + } + val remaining = + transferableContent.consume { item -> + val uri = item.uri + if (uri != null) { + onImage( + uri, + transferableContent.clipEntry.clipData.description + .getMimeType(0), + ) + true + } else { + false + } + } + return remaining + } + }, + ) diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/PlatformBackHandler.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/PlatformBackHandler.android.kt new file mode 100644 index 0000000000..fbd0460a54 --- /dev/null +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/PlatformBackHandler.android.kt @@ -0,0 +1,30 @@ +/* + * 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.commons.ui.components + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable + +@Composable +actual fun PlatformBackHandler( + enabled: Boolean, + onBack: () -> Unit, +) = BackHandler(enabled, onBack) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SetDialogToEdgeToEdge.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/SetDialogToEdgeToEdge.android.kt similarity index 83% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SetDialogToEdgeToEdge.kt rename to commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/SetDialogToEdgeToEdge.android.kt index fadf594304..0b9c109450 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SetDialogToEdgeToEdge.kt +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/SetDialogToEdgeToEdge.android.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.components +package com.vitorpamplona.amethyst.commons.ui.components import android.view.View import android.view.WindowManager @@ -27,13 +27,13 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogWindowProvider import androidx.core.view.WindowCompat -import com.vitorpamplona.amethyst.commons.ui.components.getActivityWindow import com.vitorpamplona.amethyst.commons.ui.theme.isLight @Composable -fun SetDialogToEdgeToEdge() { +actual fun SetDialogToEdgeToEdge() { val activityWindow = getActivityWindow() val dialogWindow = (LocalView.current.parent as? DialogWindowProvider)?.window val parentView = LocalView.current.parent as View @@ -58,3 +58,14 @@ fun SetDialogToEdgeToEdge() { } } } + +actual fun edgeToEdgeDialogProperties( + dismissOnBackPress: Boolean, + dismissOnClickOutside: Boolean, +): DialogProperties = + DialogProperties( + usePlatformDefaultWidth = false, + dismissOnBackPress = dismissOnBackPress, + dismissOnClickOutside = dismissOnClickOutside, + decorFitsSystemWindows = false, + ) diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.android.kt index 24d16cd38a..994d6332c0 100644 --- a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.android.kt +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.android.kt @@ -30,3 +30,9 @@ actual fun rememberShortNotice(): ShortNotice { val context = LocalContext.current return remember(context) { ShortNotice { message -> Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } } } + +@Composable +actual fun rememberLongNotice(): ShortNotice { + val context = LocalContext.current + return remember(context) { ShortNotice { message -> Toast.makeText(context, message, Toast.LENGTH_LONG).show() } } +} diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/MonthDayTime.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/MonthDayTime.android.kt new file mode 100644 index 0000000000..3e5b081006 --- /dev/null +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/MonthDayTime.android.kt @@ -0,0 +1,42 @@ +/* + * 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.commons.ui.note + +import android.text.format.DateFormat +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext + +// Explicit hour skeletons, not `j`: `j` follows the locale, the user's 12/24-hour override does not. +private val monthDayTime24 = DateSkeletonFormatter("MMMdHm") +private val monthDayTime12 = DateSkeletonFormatter("MMMdhma") + +/** Reads the 12/24-hour setting on every call: it can change under a running app. */ +@Composable +actual fun rememberMonthDayTimeFormatter(): (epochSeconds: Long) -> String { + val context = LocalContext.current + return remember(context) { + { epochSeconds -> + val formatter = if (DateFormat.is24HourFormat(context)) monthDayTime24 else monthDayTime12 + formatter.format(epochSeconds * 1000L) + } + } +} diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt index 385e7335e5..f4c13f3478 100644 --- a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt @@ -28,25 +28,35 @@ import androidx.compose.ui.platform.LocalContext import java.text.SimpleDateFormat import java.util.Date import java.util.Locale +import java.util.TimeZone /** - * Per-thread cached [SimpleDateFormat] keyed off the current default [Locale]. + * Per-thread cached [SimpleDateFormat] keyed off the current default [Locale] and time zone. * * `SimpleDateFormat` is mutable and not thread-safe, and these formatters are read from both * the UI thread (composition) and background coroutines. `ThreadLocal` gives each thread its - * own instance: no locks, no allocation per call, and a lazy rebuild on locale change. + * own instance: no locks, and a lazy rebuild on a locale or time-zone change. A formatter fixes + * its zone when built, so without the zone in the key a trip across zones would keep printing + * dates in the old one. */ actual class DateSkeletonFormatter actual constructor( private val skeleton: String, ) { - private val cache = ThreadLocal>() + private class Cached( + val locale: Locale, + val zoneId: String, + val formatter: SimpleDateFormat, + ) + + private val cache = ThreadLocal() private fun get(): SimpleDateFormat { - val current = Locale.getDefault() + val locale = Locale.getDefault() + val zoneId = TimeZone.getDefault().id val cached = cache.get() - if (cached != null && cached.first == current) return cached.second - val fresh = SimpleDateFormat(DateFormat.getBestDateTimePattern(current, skeleton), current) - cache.set(current to fresh) + if (cached != null && cached.locale == locale && cached.zoneId == zoneId) return cached.formatter + val fresh = SimpleDateFormat(DateFormat.getBestDateTimePattern(locale, skeleton), locale) + cache.set(Cached(locale, zoneId, fresh)) return fresh } diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/EmbeddedAppSurfaces.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/EmbeddedAppSurfaces.android.kt new file mode 100644 index 0000000000..7b89d6d384 --- /dev/null +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/EmbeddedAppSurfaces.android.kt @@ -0,0 +1,25 @@ +/* + * 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.commons.ui.platform + +import android.os.Build + +actual val supportsEmbeddedAppTabs: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.android.kt similarity index 86% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.kt rename to commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.android.kt index 0965ab4c75..85cd70104c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.kt +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.android.kt @@ -18,23 +18,15 @@ * 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.qrcode +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.platform.LocalView import com.vitorpamplona.amethyst.commons.ui.components.getActivityWindow -/** - * Pins the screen to full brightness, and awake, while a QR code is on it. - * - * Half of "this QR code will not scan" is the phone showing it. A dark-mode OLED at the - * auto-brightness the room asked for can put so little contrast between the black and white - * modules that the other camera's binarizer cannot separate them — and the person holding it has - * no idea that is the problem, because to a human eye the code looks perfectly clear. - */ @Composable -fun KeepScreenBrightAndAwake() { +actual fun KeepScreenBrightAndAwake() { val view = LocalView.current // NOT `(view.context as? Activity)`: under Compose the context is routinely a // ContextThemeWrapper, so that cast silently yields null and brightness never changes — diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.android.kt new file mode 100644 index 0000000000..2977a00f85 --- /dev/null +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.android.kt @@ -0,0 +1,47 @@ +/* + * 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.commons.ui.wallet + +import android.content.Intent +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.core.net.toUri +import kotlin.coroutines.cancellation.CancellationException + +@Composable +actual fun rememberWalletAppLauncher(): WalletAppLauncher { + val context = LocalContext.current + return remember(context) { + WalletAppLauncher { invoice, noWalletFound, onPaid, onError -> + try { + val intent = Intent(Intent.ACTION_VIEW, "lightning:$invoice".toUri()) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + + context.startActivity(intent) + onPaid() + } catch (e: Exception) { + if (e is CancellationException) throw e + onError(noWalletFound) + } + } + } +} diff --git a/commonsUI/src/commonMain/composeResources/values-cs/strings.xml b/commonsUI/src/commonMain/composeResources/values-cs/strings.xml index da517ef139..dc034ff669 100644 --- a/commonsUI/src/commonMain/composeResources/values-cs/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-cs/strings.xml @@ -6486,4 +6486,6 @@ Přehrát hlasovou zprávu Nahrát hlasovou zprávu Zastavit a odeslat + Hledat nebo zadat adresu + Nepojmenovaný nApplet diff --git a/commonsUI/src/commonMain/composeResources/values-de/strings.xml b/commonsUI/src/commonMain/composeResources/values-de/strings.xml index a9ecda02cd..4f08431389 100644 --- a/commonsUI/src/commonMain/composeResources/values-de/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-de/strings.xml @@ -3034,4 +3034,6 @@ Der Lightning-Service des Empfängers unter %1$s ist nicht verfügbar. Er wurde aus der Lightning-Adresse "%2$s" berechnet. Fehler: %3$s. Überprüfen Sie, ob der Server online ist und ob die Lightning-Adresse korrekt ist Kann keine Blitzrechnung von %1$s erstellen: Element pr nicht im resultierenden JSON gefunden. Blitzrechnung konnte nicht erstellt werden. Nachricht von %1$s: %2$s + Suchen oder Adresse eingeben + Unbenanntes nApplet diff --git a/commonsUI/src/commonMain/composeResources/values-eo/strings.xml b/commonsUI/src/commonMain/composeResources/values-eo/strings.xml index 797f4b70f0..46255b0be0 100644 --- a/commonsUI/src/commonMain/composeResources/values-eo/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-eo/strings.xml @@ -2995,4 +2995,6 @@ La Lightning-servo de la ricevanto ĉe %1$s ne disponeblas. Ĝi estis kalkulita el la Lightning-adreso „%2$s". Eraro: %3$s. Kontrolu ĉu la servilo funkcias kaj ĉu la Lightning-adreso estas ĝusta Ne eblas krei Lightning-fakturon de %1$s: Elemento pr ne trovita en la rezulta JSON. Ne eblas krei Lightning-fakturon. Mesaĝo de %1$s: %2$s + Serĉi aŭ enigi adreson + Sennoma nApplet diff --git a/commonsUI/src/commonMain/composeResources/values-es/strings.xml b/commonsUI/src/commonMain/composeResources/values-es/strings.xml index c3ca94cced..c278f19263 100644 --- a/commonsUI/src/commonMain/composeResources/values-es/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-es/strings.xml @@ -2995,4 +2995,6 @@ El servicio de Lightning del destinatario en %1$s no está disponible. Se calculó a partir de la dirección de Lightning "%2$s". Error: %3$s. Comprueba si el servidor está activo y si la dirección de Lightning es correcta. No se pudo crear una factura de Lightning desde %1$s: no se encontró el elemento pr en el JSON resultante. No se pudo crear una factura de Lightning. Mensaje de %1$s: %2$s. + Buscar o introducir dirección + NApplet sin título diff --git a/commonsUI/src/commonMain/composeResources/values-fa/strings.xml b/commonsUI/src/commonMain/composeResources/values-fa/strings.xml index 29be72a76e..318f5e5a88 100644 --- a/commonsUI/src/commonMain/composeResources/values-fa/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-fa/strings.xml @@ -2995,4 +2995,6 @@ خدمات لایتنینگ %1$s گیرنده دردسترس نیست. از آدرس لایتنینگ "%2$s" محاسبه شد. خطا: %3$s. بررسی کنید آیا سرور کار می کند و آیا آدرس لایتنینگ صحیح است. نمی توان صورتحساب لایتنینگ از %1$s ساخت. المنت pr در JSON بدست آمده یافت نشد. صورت حساب لایتنینگ ساخته نشد. پیغام از %1$s: %2$s + جستجو یا وارد کردن آدرس + nApplet بی‌عنوان diff --git a/commonsUI/src/commonMain/composeResources/values-fr/strings.xml b/commonsUI/src/commonMain/composeResources/values-fr/strings.xml index 136defe6e8..81ae75c3a4 100644 --- a/commonsUI/src/commonMain/composeResources/values-fr/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-fr/strings.xml @@ -2995,4 +2995,6 @@ Le service lightning du récepteur à %1$s n'est pas disponible. Il a été calculé à partir de l'adresse lightning "%2$s". Erreur: %3$s. Vérifiez si le serveur est en ligne et si l'adresse lightning est correcte Impossible de créer une facture lightning avec %1$s. Élément pr introuvable dans le JSON résultant. Impossible de créer une facture lightning. Message de %1$s: %2$s + Rechercher ou saisir une adresse + nApplet sans titre diff --git a/commonsUI/src/commonMain/composeResources/values-in/strings.xml b/commonsUI/src/commonMain/composeResources/values-in/strings.xml index d957d12d1c..4195ff78f8 100644 --- a/commonsUI/src/commonMain/composeResources/values-in/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-in/strings.xml @@ -2958,4 +2958,6 @@ Layanan lightning penerima pada %1$s tidak tersedia. Itu dihitung dari alamat petir "%2$s". Kesalahan: %3$s. Periksa apakah server aktif dan apakah alamat lightning sudah benar Tidak dapat membuat tagihan Lightning dari %1$s: Elemen pr tidak ditemukan dalam JSON yang dihasilkan. Tidak dapat membuat tagihan Lightning. Pesan dari %1$s: %2$s + Cari atau masukkan alamat + nApplet Tanpa Judul diff --git a/commonsUI/src/commonMain/composeResources/values-ja/strings.xml b/commonsUI/src/commonMain/composeResources/values-ja/strings.xml index 7791621a8a..22a1767278 100644 --- a/commonsUI/src/commonMain/composeResources/values-ja/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-ja/strings.xml @@ -2958,4 +2958,6 @@ %1$s の受信者の Lightning サービスが利用できません。Lightning アドレス "%2$s" から計算されました。エラー: %3$s。サーバーが稼働しているか、Lightning アドレスが正しいかを確認してください %1$s から Lightning 請求書を作成できませんでした: 結果の JSON に要素 pr が見つかりませんでした。 Lightning 請求書を作成できませんでした。%1$s からのメッセージ: %2$s + 検索またはアドレスを入力 + 無題の nApplet diff --git a/commonsUI/src/commonMain/composeResources/values-nl-rBE/strings.xml b/commonsUI/src/commonMain/composeResources/values-nl-rBE/strings.xml index f6bda663e7..9a4af42668 100644 --- a/commonsUI/src/commonMain/composeResources/values-nl-rBE/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-nl-rBE/strings.xml @@ -2995,4 +2995,6 @@ De Lightning-service van de ontvanger op %1$s is niet beschikbaar. Dit werd berekend vanuit Lightning-adres "%2$s". Fout: %3$s. Controleer of de server online is en het adres correct is. Kon geen Lightning-invoice aanmaken van %1$s: element "pr" niet gevonden in JSON. Kon geen Lightning-invoice aanmaken. Bericht van %1$s: %2$s + Zoeken of adres invoeren + Naamloze nApplet diff --git a/commonsUI/src/commonMain/composeResources/values-nl/strings.xml b/commonsUI/src/commonMain/composeResources/values-nl/strings.xml index 2720341499..b0948cc7a7 100644 --- a/commonsUI/src/commonMain/composeResources/values-nl/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-nl/strings.xml @@ -2995,4 +2995,6 @@ De Lightning service van de ontvanger bij %1$s is niet beschikbaar. Het werd berekend op basis van het Lightning Adress "%2$s". Fout: %3$s. Controleer of de server klaar is en of het Lightning Adress juist is Kan geen Lightning invoice aanmaken van %1$s: PR-element niet gevonden in de JSON. Kan geen Lightning invoice aanmaken. Bericht van %1$s: %2$s + Zoeken of adres invoeren + Naamloze nApplet diff --git a/commonsUI/src/commonMain/composeResources/values-ru/strings.xml b/commonsUI/src/commonMain/composeResources/values-ru/strings.xml index 7c8d0ca239..3b9e5a7240 100644 --- a/commonsUI/src/commonMain/composeResources/values-ru/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-ru/strings.xml @@ -3069,4 +3069,6 @@ Lightning-сервис получателя по адресу %1$s недоступен. Он был вычислен из Lightning-адреса "%2$s". Ошибка: %3$s. Проверьте, работает ли сервер и правильно ли указан Lightning-адрес Не удалось создать Lightning-счёт от %1$s: элемент pr не найден в полученном JSON. Не удалось создать Lightning-счёт. Сообщение от %1$s: %2$s + Поиск или введите адрес + nApplet без названия diff --git a/commonsUI/src/commonMain/composeResources/values-ta/strings.xml b/commonsUI/src/commonMain/composeResources/values-ta/strings.xml index a6fa3f2130..1de7149e7e 100644 --- a/commonsUI/src/commonMain/composeResources/values-ta/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-ta/strings.xml @@ -2995,4 +2995,6 @@ %1$s-ல் பெறுநரின் lightning சேவை கிடைக்கவில்லை. இது lightning முகவரி "%2$s" இலிருந்து கணக்கிடப்பட்டது. பிழை: %3$s. சர்வர் இயங்குகிறதா மற்றும் lightning முகவரி சரியானதா என சரிபாருங்கள் %1$s-இலிருந்து lightning invoice உருவாக்க முடியவில்லை: முடிவு JSON-ல் pr உறுப்பு கிடைக்கவில்லை. Lightning invoice உருவாக்க முடியவில்லை. %1$s-இலிருந்து செய்தி: %2$s + தேடவும் அல்லது முகவரியை உள்ளிடவும் + தலைப்பற்ற nApplet diff --git a/commonsUI/src/commonMain/composeResources/values-th/strings.xml b/commonsUI/src/commonMain/composeResources/values-th/strings.xml index cdb57bb5be..c7d90434d1 100644 --- a/commonsUI/src/commonMain/composeResources/values-th/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-th/strings.xml @@ -2958,4 +2958,6 @@ บริการ Lightning ของ %1$s แลจะไม่พร้อมใช้งานจาก "%2$s". Error: %3$s. โปรดตรวจสอบผู้ให้บริการ หรือที่อยู่ Lightning ให้ถูกตต้อง ไม่สามารถสร้างบิลชำระในการส่ง zap จาก %1$s เนื่องจากไม่พบ PR บน JSON ไม่สามารถสร้าง lightning invoice ข้อความจาก %1$s: %2$s + ค้นหาหรือป้อนที่อยู่ + nApplet ไม่มีชื่อ diff --git a/commonsUI/src/commonMain/composeResources/values-tr/strings.xml b/commonsUI/src/commonMain/composeResources/values-tr/strings.xml index a54077a881..152a231b8b 100644 --- a/commonsUI/src/commonMain/composeResources/values-tr/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-tr/strings.xml @@ -2995,4 +2995,6 @@ %1$s adresindeki alıcının Lightning hizmeti kullanılamıyor. Bu adres "%2$s" Lightning adresinden hesaplandı. Hata: %3$s. Sunucunun çalışıp çalışmadığını ve Lightning adresinin doğru olup olmadığını kontrol edin. %1$s adresinden Lightning faturası oluşturulamadı: Sonuçtaki JSON'da pr öğesi bulunamadı. Lightning faturası oluşturulamadı. %1$s'den gelen mesaj: %2$s + Ara veya adres gir + Başlıksız nApplet diff --git a/commonsUI/src/commonMain/composeResources/values-uk/strings.xml b/commonsUI/src/commonMain/composeResources/values-uk/strings.xml index 8e96de5dd5..28c1111f75 100644 --- a/commonsUI/src/commonMain/composeResources/values-uk/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-uk/strings.xml @@ -3069,4 +3069,6 @@ Lightning-сервіс отримувача за адресою %1$s недоступний. Він був розрахований із lightning-адреси "%2$s". Помилка: %3$s. Перевірте, чи працює сервер і чи правильна lightning-адреса Неможливо створити lightning-рахунок від %1$s: Елемент pr не знайдено у відповідному JSON. Неможливо створити lightning-рахунок. Повідомлення від %1$s: %2$s + Пошук або введіть адресу + NApplet без назви diff --git a/commonsUI/src/commonMain/composeResources/values-zh/strings.xml b/commonsUI/src/commonMain/composeResources/values-zh/strings.xml index 6d7ae350e0..3ae6e070a5 100644 --- a/commonsUI/src/commonMain/composeResources/values-zh/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-zh/strings.xml @@ -2999,4 +2999,6 @@ %1$s 的接收者闪电服务不可用。它是根据雷电地址"%2$s"计算的。 错误: %3$s。请检查服务器是否已经上线,闪电地址是否正确 无法从 %1$s 创建闪电发票:在生成的 JSON 中找不到付款要求元素。 无法创建闪电发票。来自 %1$s 的消息:%2$s + 搜索或输入地址 + 未命名nApplet diff --git a/commonsUI/src/commonMain/composeResources/values/strings.xml b/commonsUI/src/commonMain/composeResources/values/strings.xml index 799c2bf27a..c3c70cbcfc 100644 --- a/commonsUI/src/commonMain/composeResources/values/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values/strings.xml @@ -6294,4 +6294,13 @@ Play voice note Record a voice note Stop and send + Search or enter address + Untitled nApplet + Site permissions + Use your camera + Use your microphone + Know your approximate location + Ask + Allowed + Blocked diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteNostrAppPreloader.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/favorites/FavoriteNostrAppPreloader.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteNostrAppPreloader.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/favorites/FavoriteNostrAppPreloader.kt index ded8b58a2f..362234a509 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/FavoriteNostrAppPreloader.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/favorites/FavoriteNostrAppPreloader.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.favorites +package com.vitorpamplona.amethyst.commons.favorites import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/TopNavFilterState.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/TopNavFilterState.kt index c370932351..98f0917163 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/TopNavFilterState.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/TopNavFilterState.kt @@ -24,7 +24,6 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.AddressableNote -import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.nip51Lists.interestSets.InterestSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter @@ -295,7 +294,6 @@ class TopNavFilterState( livePeopleListsFlow, liveInterestFlows, ) { peopleLists, interests -> - LocalCache.appHost.assertNotMainThread() emit( listOf( listOf(allFollows, userFollows, kind3Follows, aroundMe, teleport, globalFollow), @@ -308,7 +306,6 @@ class TopNavFilterState( private val _badgeRoutes = livePeopleListsFlow.transform { peopleLists -> - LocalCache.appHost.assertNotMainThread() emit( listOf( listOf(allFollows, userFollows, kind3Follows, globalFollow, mineFollow), @@ -320,7 +317,6 @@ class TopNavFilterState( private val _communityRoutes = livePeopleListsFlow.transform { peopleLists -> - LocalCache.appHost.assertNotMainThread() emit( listOf( listOf(allFollows, userFollows, kind3Follows, globalFollow, mineFollow), @@ -335,7 +331,6 @@ class TopNavFilterState( livePeopleListsFlow, liveInterestFlows, ) { peopleLists, interests -> - LocalCache.appHost.assertNotMainThread() emit( listOf( // Same content-style catalog as kind3GlobalPeopleRoutes, plus "Mine" so the @@ -353,7 +348,6 @@ class TopNavFilterState( livePeopleListsFlow, liveInterestFlows, ) { peopleLists, interests -> - LocalCache.appHost.assertNotMainThread() emit( listOf( // Git repository announcements can be narrowed by author, hashtag and geohash, @@ -371,7 +365,6 @@ class TopNavFilterState( livePeopleListsFlow, liveInterestFlows, ) { peopleLists, interests -> - LocalCache.appHost.assertNotMainThread() emit( listOf( // Workout records can be narrowed by author, hashtag and geohash, so this @@ -389,7 +382,6 @@ class TopNavFilterState( livePeopleListsFlow, liveInterestFlows, ) { peopleLists, interests -> - LocalCache.appHost.assertNotMainThread() emit( listOf( // Highlights can be narrowed by author, hashtag and geohash, so this mirrors @@ -408,7 +400,6 @@ class TopNavFilterState( liveInterestFlows, account.relayGroupList.liveRelayGroupServers, ) { peopleLists, interests, joinedServers -> - LocalCache.appHost.assertNotMainThread() // A relay chip per host relay of every group the user joined (kind-10009), so they can // browse the OTHER groups on those relays without first having to Favorite them. Deduped // against relays that are already chips because they were favorited (kind-10012, already @@ -442,7 +433,6 @@ class TopNavFilterState( livePeopleListsFlow, liveInterestFlows, ) { peopleLists, interests -> - LocalCache.appHost.assertNotMainThread() emit( listOf( // Same content-style catalog as kind3GlobalPeopleRoutes, plus "Mine" so the @@ -457,7 +447,6 @@ class TopNavFilterState( private val _kind3GlobalPeople = livePeopleListsFlow.transform { peopleLists -> - LocalCache.appHost.assertNotMainThread() emit( listOf( listOf(allFollows, userFollows, kind3Follows, aroundMe, teleport, globalFollow), @@ -473,7 +462,6 @@ class TopNavFilterState( // and AroundMe — none of which can match a tag-less manifest — are deliberately left out. private val _authorOnlyRoutes = livePeopleListsFlow.transform { peopleLists -> - LocalCache.appHost.assertNotMainThread() emit( listOf( listOf(allFollows, userFollows, kind3Follows, globalFollow, mineFollow), @@ -485,7 +473,6 @@ class TopNavFilterState( private val _notificationLists = livePeopleListsFlow.transform { peopleLists -> - LocalCache.appHost.assertNotMainThread() emit( listOf( listOf(allFollows, userFollows, kind3Follows, aroundMe, teleport, selectedFollow, globalFollow), diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/notifications/NotificationSummaryState.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/notifications/NotificationSummaryState.kt index 5dd59a4eb6..be93ba96be 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/notifications/NotificationSummaryState.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/notifications/NotificationSummaryState.kt @@ -89,8 +89,6 @@ class NotificationSummaryState( fun today(): String = LocalClock.today().ymd() suspend fun initializeSuspend() { - LocalCache.appHost.assertNotMainThread() - val days = LocalClock.epochDayCounter() val currentUser = user.pubkeyHex @@ -182,8 +180,6 @@ class NotificationSummaryState( } suspend fun addToStatsSuspend(newBlockNotes: Set>) { - LocalCache.appHost.assertNotMainThread() - val days = LocalClock.epochDayCounter() val currentUser = user.pubkeyHex @@ -286,8 +282,6 @@ class NotificationSummaryState( } private suspend fun refreshChartModel() { - LocalCache.appHost.assertNotMainThread() - val today = LocalClock.today() val dataAxisLabelIndexes = listOf(-6, -5, -4, -3, -2, -1, 0) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthComposeHelpers.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/authCommand/compose/RelayAuthComposeHelpers.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthComposeHelpers.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/authCommand/compose/RelayAuthComposeHelpers.kt index 4ac7462573..92834fc256 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/compose/RelayAuthComposeHelpers.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/authCommand/compose/RelayAuthComposeHelpers.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.service.relayClient.authCommand.compose +package com.vitorpamplona.amethyst.commons.relayClient.authCommand.compose import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -48,7 +48,7 @@ import org.jetbrains.compose.resources.StringResource * settings screen, which both render a person by pubkey while their metadata streams in. */ @Composable -internal fun LoadRelayAuthUser( +fun LoadRelayAuthUser( pubkey: HexKey, content: @Composable (User?) -> Unit, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/UsageKeys.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/resourceusage/UsageKeys.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/UsageKeys.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/resourceusage/UsageKeys.kt index fd222cf993..5fb71aac2e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/UsageKeys.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/resourceusage/UsageKeys.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.service.resourceusage +package com.vitorpamplona.amethyst.commons.service.resourceusage import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.quartz.nip01Core.relay.client.single.basic.BasicRelayClient @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.MachineReadabl import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.MachineReadablePrefix.RESTRICTED import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.MachineReadablePrefix.UNSUPPORTED import com.vitorpamplona.quartz.nip66RelayMonitor.reachability.RelayObserver -import java.util.concurrent.ConcurrentHashMap +import com.vitorpamplona.quartz.utils.concurrent.ConcurrentMap /** * Counter-key grammar for the resource-usage ledger. Keys are flat strings so @@ -489,8 +489,8 @@ object UsageKeys { private const val UP = "up" private const val DOWN = "down" - private val VERB_UP_KEYS = ConcurrentHashMap>() - private val VERB_DOWN_KEYS = ConcurrentHashMap>() + private val VERB_UP_KEYS = ConcurrentMap>() + private val VERB_DOWN_KEYS = ConcurrentMap>() /** * `relay.purpose.home_feed.sent` / `.bytes` — REQ frames and REQ bytes by the @@ -562,7 +562,7 @@ object UsageKeys { private val PURPOSE_SUFFIXES = arrayOf("sent", "bytes", "down", "downn", "dupbytes") - private val PURPOSE_KEYS = ConcurrentHashMap>() + private val PURPOSE_KEYS = ConcurrentMap>() /** The five `relay.purpose..*` keys, indexed by the `P_` constants above. */ private fun purposeKeys(purpose: String): Array = PURPOSE_KEYS.getOrPut(purpose) { Array(PURPOSE_SUFFIXES.size) { "relay.purpose.$purpose.${PURPOSE_SUFFIXES[it]}" } } diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/UriToRoute.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/UriToRoute.kt new file mode 100644 index 0000000000..cd01f9babe --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/UriToRoute.kt @@ -0,0 +1,388 @@ +/* + * 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.commons.ui + +import com.vitorpamplona.amethyst.commons.actions.ConcordActions +import com.vitorpamplona.amethyst.commons.model.Account +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.navigation.Route +import com.vitorpamplona.amethyst.commons.model.navigation.routeFor +import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.commons.ui.navigation.findQueryParameterValue +import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor +import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeForPointer +import com.vitorpamplona.quartz.buzz.invite.BuzzInviteLink +import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser +import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress +import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed +import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NNote +import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile +import com.vitorpamplona.quartz.nip19Bech32.entities.NPub +import com.vitorpamplona.quartz.nip29RelayGroups.GroupInviteLink +import com.vitorpamplona.quartz.nip29RelayGroups.GroupNAddrInvite +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent +import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectURI +import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect +import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent +import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent +import com.vitorpamplona.quartz.nip73ExternalIds.urls.UrlId +import com.vitorpamplona.quartz.utils.UriParser +import com.vitorpamplona.quartz.utils.UrlEncoder +import kotlinx.coroutines.CancellationException + +private const val NOSTR_URI_PREFIX = "nostr:" + +fun isNotificationRoute(uri: String) = uri.startsWith("notifications", true) || uri.startsWith("nostr:notifications", true) + +fun isHashtagRoute(uri: String) = uri.startsWith("hashtag?id=") || uri.startsWith("nostr:hashtag?id=") + +/** + * A markdown link target that is a bare hashtag fragment, e.g. the `#NostrMultiplayerGames` in + * `[Games](#NostrMultiplayerGames)`. Returns the tag without the leading `#`, or null when the + * uri is anything else (full URLs with anchors don't start with `#`). Reuses the same character + * class that linkifies #hashtags in plain text, so validity matches how tags parse everywhere else. + */ +fun fragmentHashtagOrNull(uri: String): String? { + val match = RichTextParser.hashTagsPattern.matchEntire(uri) ?: return null + if (!match.groups[2]?.value.isNullOrEmpty()) return null + return match.groups[1]?.value +} + +fun isUrlRoute(uri: String) = uri.startsWith("url?id=") || uri.startsWith("nostr:url?id=") + +/** + * A private chatroom, addressed by its participants rather than by a message. + * Posted by DM notifications — see `NotificationRoutes.chatroomUri` for why a DM + * cannot deep-link through its own note. + */ +fun isChatroomRoute(uri: String) = uri.startsWith("chatroom?id=") || uri.startsWith("nostr:chatroom?id=") + +/** + * Note what this deliberately does *not* do: register the room on the account. + * + * `routeToMessage` would, and that is right for the in-app callers, but wrong here twice over. + * `uriToRoute` runs against whichever account is current, *before* `?account=` is read and the + * switch happens (`AppNavigation.NavigateIfIntentRequested`), so a DM notification for account B + * tapped while A is on screen would insert an empty conversation into **A**'s message list. And + * `nostr:` is an exported, browsable scheme, so any web page could post + * `nostr:chatroom?id=&account=…` and inject a room of its choosing. + * + * Nothing needs it: `ChatroomFeedFilter.chatroom()` calls `getOrCreatePrivateChatroom` when the + * screen actually opens, by which point the switch has happened and the room lands on the right + * account. + * + * The ids are still checked here — a room key is a set of pubkeys, so anything that isn't one is + * not a room, and a bad deep link should resolve to nothing rather than to an unopenable screen. + */ +fun chatroomRoute(uri: String): Route? { + val users = + uri + .findQueryParameterValue("id") + ?.split(',') + ?.map { it.trim() } + ?.takeIf { it.isNotEmpty() && it.all(::isPubKeyHex) } + ?.toSet() ?: return null + + return Route.Room(ChatroomKey(users)) +} + +/** A bare 32-byte lowercase-hex pubkey, the only thing a chatroom key is made of. */ +private fun isPubKeyHex(value: String) = value.length == 64 && value.all { it in '0'..'9' || it in 'a'..'f' } + +/** + * A NIP-17 private note or reply, addressed by the rumor's own id because its `nevent` + * names the undeliverable gift wrap instead — see `NotificationRoutes.privateNoteUri`. + */ +fun isPrivateNoteRoute(uri: String) = uri.startsWith("privatenote?id=") || uri.startsWith("nostr:privatenote?id=") + +fun privateNoteRoute(uri: String): Route? { + val id = uri.findQueryParameterValue("id") ?: return null + return Route.EventRedirect(id, isPrivate = true) +} + +fun isConnectedAppRoute(uri: String) = uri.startsWith("connectedapp?coordinate=") || uri.startsWith("nostr:connectedapp?coordinate=") + +/** + * The Connected Apps permission-detail route for an app coordinate (`pubkey:dtag` for a napplet/nsite, + * `browser:` for a web client). Fired by the sandbox host so a running full-screen surface can + * jump straight to its editable permissions; the coordinate is URL-encoded into the [coordinate] param. + */ +fun connectedAppRoute(uri: String): Route.ConnectedAppDetail? { + val coordinate = + runCatching { + val raw = uri.removePrefix(NOSTR_URI_PREFIX).findQueryParameterValue("coordinate") ?: return null + UrlEncoder.decode(raw) + }.getOrNull()?.takeIf { it.isNotBlank() } ?: return null + + return Route.ConnectedAppDetail(coordinate) +} + +fun urlRoute(uri: String): Route.Url? { + val url = + runCatching { + val rawUrl = uri.removePrefix(NOSTR_URI_PREFIX).findQueryParameterValue("id") ?: return null + UrlEncoder.decode(rawUrl) + }.getOrNull() ?: return null + + return UrlId.toScopeOrNull(url)?.let { Route.Url(it) } +} + +fun isWalletConnectRoute(uri: String) = uri.startsWith("dlnwc?value=") || uri.startsWith("amethyst+walletconnect:dlnwc?value=") || uri.startsWith("amethyst+walletconnect://dlnwc?value=") + +fun isMarmotGroupRoute(uri: String) = uri.startsWith("marmot:") + +/** + * Tapping the always-on service notification. That notification's whole subject is the relay + * connections it is holding open, so it lands on the screen that explains them rather than on + * whatever tab happened to be last open. + */ +fun isActiveSubscriptionsRoute(uri: String) = uri.startsWith("activesubs", true) || uri.startsWith("nostr:activesubs", true) + +/** + * The launcher shortcut (res/xml/shortcuts.xml) fires `amethyst:scanqr`, caught by MainActivity's + * existing scheme filter. A pseudo-uri rather than a hardcoded component, because debug and + * benchmark builds carry an applicationId suffix a hardcoded targetPackage would miss, and + * manifest placeholders are not substituted into resource XML. + * + * `nostr:scanqr` is still accepted so an older pinned shortcut keeps working, but the shortcut + * itself uses our own scheme: an implicit `nostr:` VIEW intent can be answered by any Nostr client + * installed alongside us. + */ +fun isScanQrRoute(uri: String) = + uri.equals("scanqr", true) || + uri.equals("amethyst:scanqr", true) || + uri.equals("nostr:scanqr", true) + +private val MARMOT_HEX = Regex("^[0-9a-fA-F]+$") + +fun uriToRoute( + uri: String, + account: Account, +): Route? { + if (isNotificationRoute(uri)) { + val scrollTo = uri.findQueryParameterValue("scrollTo") + return Route.Notification(scrollToEventId = scrollTo) + } + if (isActiveSubscriptionsRoute(uri)) { + return Route.ActiveSubscriptions + } + if (isScanQrRoute(uri)) { + return Route.QRDisplay(account.signer.pubKey, startScanning = true) + } + if (isHashtagRoute(uri)) { + return Route.Hashtag(uri.removePrefix(NOSTR_URI_PREFIX).removePrefix("hashtag?id=").lowercase()) + } + fragmentHashtagOrNull(uri)?.let { + return Route.Hashtag(it.lowercase()) + } + if (isUrlRoute(uri)) { + return urlRoute(uri) + } + if (isChatroomRoute(uri)) { + return chatroomRoute(uri) + } + if (isPrivateNoteRoute(uri)) { + return privateNoteRoute(uri) + } + if (isConnectedAppRoute(uri)) { + return connectedAppRoute(uri) + } + + // A scanned/opened `nostrconnect://` offer is an app asking to connect to our signer: open the + // NIP-46 signer screen and let it run the pairing (it enables the signer as part of connecting). + if (uri.startsWith(NostrConnectURI.NOSTRCONNECT_SCHEME)) { + return Route.Nip46Signer(connectUri = uri) + } + + relayGroupInviteRoute(uri)?.let { return it } + concordInviteRoute(uri)?.let { return it } + buzzInviteRoute(uri)?.let { return it } + + val parsedNip19 = Nip19Parser.uriToRoute(uri) + val nip19 = parsedNip19?.entity + if (nip19 != null) { + LocalCache.consume(nip19) + + val route = + when (nip19) { + is NPub -> { + Route.Profile(nip19.hex) + } + + is NProfile -> { + Route.Profile(nip19.hex) + } + + is NNote -> { + Route.Note(nip19.hex) + } + + is NEvent -> { + val note = LocalCache.getOrCreateNote(nip19.hex) + // Only fall back to the pointer's own kind while the body is missing: once the + // event is cached it may belong somewhere the kind alone can't name (a channel, + // a chatroom), and routeFor knows that. + if (note.event == null) { + routeForPointer(nip19.kind, nip19.hex) ?: Route.EventRedirect(nip19.hex) + } else { + routeFor(note = note, loggedIn = account) ?: Route.EventRedirect(nip19.hex) + } + } + + is NAddress -> { + relayGroupDirectRoute(nip19, parsedNip19.additionalChars) + ?: routeFor( + note = LocalCache.getOrCreateAddressableNote(nip19.address()), + loggedIn = account, + ) ?: calendarDirectRoute(nip19) ?: Route.EventRedirect(nip19.aTag()) + } + + is NEmbed -> { + val noteEvent = nip19.event + if (noteEvent is AddressableEvent) { + routeFor( + note = LocalCache.getOrCreateAddressableNote(noteEvent.address()), + loggedIn = account, + ) ?: Route.EventRedirect(noteEvent.addressTag()) + } else { + routeFor( + note = LocalCache.getOrCreateNote(nip19.event.id), + loggedIn = account, + ) ?: Route.EventRedirect(nip19.event.id) + } + } + + else -> { + null + } + } + + if (route != null) { + return route + } + } + + if (isMarmotGroupRoute(uri)) { + // marmot:?account= + val groupHex = + uri + .removePrefix("marmot:") + .substringBefore("?") + .substringBefore("&") + if (groupHex.matches(MARMOT_HEX)) { + return Route.MarmotGroupChat(groupHex) + } + } + + if (isWalletConnectRoute(uri)) { + try { + val url = UriParser(uri) + val nip47Uri = url.getQueryParameter("value")?.firstOrNull() + if (nip47Uri != null) { + Nip47WalletConnect.parse(nip47Uri) + return Route.WalletAddNwc(nip47Uri) + } + } catch (e: Exception) { + if (e is CancellationException) throw e + } + } + + try { + Nip47WalletConnect.parse(uri) + return Route.WalletAddNwc(uri) + } catch (e: Exception) { + if (e is CancellationException) throw e + } + + return null +} + +/** + * Direct route for an `naddr` whose event hasn't arrived in [LocalCache] yet. When a notification + * is tapped (or a `nostr:naddr…` deep link arrives) for a calendar appointment we know is kind + * 31922/31923, route straight to the dedicated detail screen instead of bouncing through + * [Route.EventRedirect]. The detail screen issues its own per-event subscription, so the user + * sees the calendar-specific loading placeholder while the event arrives, not the generic + * redirect screen. + */ +private fun calendarDirectRoute(nip19: NAddress): Route? = + when (nip19.kind) { + CalendarTimeSlotEvent.KIND, + CalendarDateSlotEvent.KIND, + -> Route.CalendarEventDetail(nip19.kind, nip19.author, nip19.dTag) + else -> null + } + +/** + * Direct route for a NIP-29 group `naddr` (kind 39000) shared as a link. The group + * only exists on its host relay, so the naddr's relay hint is mandatory — the group + * id alone can't be resolved. With a hint we open the group chat straight away + * (cold start included); without one there's nowhere to look, so fall through. + */ +private fun relayGroupDirectRoute( + nip19: NAddress, + additionalChars: String?, +): Route? { + if (nip19.kind != GroupMetadataEvent.KIND) return null + val relay = nip19.relay.firstOrNull() ?: return null + // The spec allows an invite code appended as `naddr1…?invite=`; it arrives + // in the trailing chars after the bech32 body. Carry it so the group auto-joins. + val inviteCode = GroupNAddrInvite.parse(additionalChars) + return Route.RelayGroup(nip19.dTag, relay.url, inviteCode = inviteCode) +} + +/** + * Direct route for a NIP-29 group invite link in the de-facto `'[?code=]` + * form shared by Wisp and 0xchat (e.g. `wss://groups.0xchat.com'abc123`). The link carries its + * own host relay, so we can open the group straight away. Returns null for anything that isn't + * this exact shape, so it's safe to try before the nostr-entity parser. + */ +private fun relayGroupInviteRoute(uri: String): Route? { + val link = GroupInviteLink.parse(uri.removePrefix(NOSTR_URI_PREFIX)) ?: return null + return Route.RelayGroup(link.groupId, link.relayUrl.url, inviteCode = link.code) +} + +/** + * A shared Concord invite URL (`…/invite/#`). Cheap substring gates + * keep the parse off the hot path; the whole URL (fragment included) is carried into + * the route so the redeem flow still has the unlock token. + */ +private fun concordInviteRoute(uri: String): Route? = + if (uri.contains("/invite/") && uri.contains('#') && ConcordActions.parseInviteLink(uri) != null) { + Route.ConcordInvite(uri) + } else { + null + } + +/** + * A Buzz workspace invite (`https:///invite/`) — a plain `/invite/` https URL with + * no fragment, so disjoint from the Concord shape above. Opens the in-app join flow + * ([Route.BuzzInvite]) instead of the external browser so the claim signs with the user's key. + */ +private fun buzzInviteRoute(uri: String): Route? = + if (uri.contains("/invite/") && BuzzInviteLink.parse(uri) != null) { + Route.BuzzInvite(uri) + } else { + null + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/NewUserMetadataScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/NewUserMetadataScreen.kt index 9e95251d74..8fe1295de8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/NewUserMetadataScreen.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.actions +package com.vitorpamplona.amethyst.commons.ui.actions import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically @@ -46,11 +46,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res @@ -75,13 +73,14 @@ import com.vitorpamplona.amethyst.commons.resources.twitter import com.vitorpamplona.amethyst.commons.resources.twitter_proof_url_template import com.vitorpamplona.amethyst.commons.resources.username import com.vitorpamplona.amethyst.commons.resources.website_url +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectSingleFromGallery +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -89,9 +88,8 @@ fun NewUserMetadataScreen( nav: INav, accountViewModel: AccountViewModel, ) { - val postViewModel: NewUserMetadataViewModel = viewModel() + val postViewModel: NewUserMetadataViewModel = rememberViewModel { NewUserMetadataViewModel() } postViewModel.init(accountViewModel) - val context = LocalContext.current val socialExpanded = rememberSaveable { mutableStateOf(false) } @@ -213,7 +211,7 @@ fun NewUserMetadataScreen( tint = MaterialTheme.colorScheme.placeholderText, modifier = Modifier.padding(start = 5.dp), ) { - postViewModel.uploadForPicture(it, context, onError = accountViewModel.toastManager::toast) + postViewModel.uploadForPicture(it, accountViewModel.host.mediaUploader, onError = accountViewModel.toastManager::toast) } }, singleLine = true, @@ -238,7 +236,7 @@ fun NewUserMetadataScreen( tint = MaterialTheme.colorScheme.placeholderText, modifier = Modifier.padding(start = 5.dp), ) { - postViewModel.uploadForBanner(it, context, onError = accountViewModel.toastManager::toast) + postViewModel.uploadForBanner(it, accountViewModel.host.mediaUploader, onError = accountViewModel.toastManager::toast) } }, singleLine = true, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/NewUserMetadataViewModel.kt similarity index 58% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/NewUserMetadataViewModel.kt index 049774718c..a2e4f3721a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/NewUserMetadataViewModel.kt @@ -18,35 +18,21 @@ * 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.actions +package com.vitorpamplona.amethyst.commons.ui.actions -import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.Account -import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType -import com.vitorpamplona.amethyst.commons.resources.Res -import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed -import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled -import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading -import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MetadataStripper -import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader -import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.identityClaims -import kotlin.coroutines.cancellation.CancellationException class NewUserMetadataViewModel : ViewModel() { private lateinit var accountViewModel: AccountViewModel @@ -164,13 +150,13 @@ class NewUserMetadataViewModel : ViewModel() { fun uploadForPicture( uri: SelectedMedia, - context: Context, + uploader: MediaUploader, onError: (String, String) -> Unit, ) { accountViewModel.launchSigner { upload( galleryUri = uri, - context = context, + uploader = uploader, onError = onError, )?.let { picture.value = it @@ -180,13 +166,13 @@ class NewUserMetadataViewModel : ViewModel() { fun uploadForBanner( uri: SelectedMedia, - context: Context, + uploader: MediaUploader, onError: (String, String) -> Unit, ) { accountViewModel.launchSigner { upload( galleryUri = uri, - context = context, + uploader = uploader, onError = onError, )?.let { banner.value = it @@ -196,14 +182,14 @@ class NewUserMetadataViewModel : ViewModel() { fun uploadPictureAndSave( uri: SelectedMedia, - context: Context, + uploader: MediaUploader, onError: (String, String) -> Unit, ) { load() accountViewModel.launchSigner { upload( galleryUri = uri, - context = context, + uploader = uploader, onError = onError, )?.let { picture.value = it @@ -215,81 +201,12 @@ class NewUserMetadataViewModel : ViewModel() { private suspend fun upload( galleryUri: SelectedMedia, - context: Context, + uploader: MediaUploader, onError: (String, String) -> Unit, ): String? { isUploadingImageForPicture = true - - val strippingResult = - if (account.settings.stripLocationOnUpload) { - try { - MetadataStripper.strip(galleryUri.uri, galleryUri.mimeType, context.applicationContext) - } catch (e: AvifMetadataNotVerifiableException) { - isUploadingImageForPicture = false - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName), - ) - return null - } - } else { - null - } - - val sourceUri = - if (account.settings.stripLocationOnUpload && - strippingResult != null && - !strippingResult.stripped - ) { - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.metadata_strip_failed_upload_cancelled), - ) - return null - } else { - strippingResult?.uri ?: galleryUri.uri - } - - val compResult = MediaCompressor().compress(sourceUri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext) - return try { - val result = - if (account.settings.defaultFileServer.type == ServerType.NIP96) { - Nip96Uploader().upload( - uri = compResult.uri, - contentType = compResult.contentType, - size = compResult.size, - alt = null, - sensitiveContent = null, - serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = accountViewModel.httpClientBuilder::okHttpClientForUploads, - onProgress = {}, - httpAuth = account::createHTTPAuthorization, - context = context, - ) - } else { - BlossomUploader().upload( - uri = compResult.uri, - contentType = compResult.contentType, - size = compResult.size, - alt = null, - sensitiveContent = null, - serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = accountViewModel.httpClientBuilder::okHttpClientForUploads, - httpAuth = account::createBlossomUploadAuth, - context = context, - ) - } - - if (result.url == null) { - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading)) - } - - result.url - } catch (e: Exception) { - if (e is CancellationException) throw e - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName) - null + uploadToDefaultServer(galleryUri, account, uploader, onError) } finally { isUploadingImageForPicture = false } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromFiles.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SelectFromFiles.kt similarity index 69% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromFiles.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SelectFromFiles.kt index e652175c14..c4458c2768 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromFiles.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SelectFromFiles.kt @@ -18,32 +18,26 @@ * 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.actions.uploads +package com.vitorpamplona.amethyst.commons.ui.actions.uploads -import android.net.Uri -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.height import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.upload_file +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation import com.vitorpamplona.amethyst.commons.ui.stringRes import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList -import java.util.concurrent.atomic.AtomicBoolean @Composable fun SelectFromFiles( @@ -94,38 +88,13 @@ private fun FileSelectButton( } } +/** Opens the platform's document picker for audio and PDF files; reports an empty list on cancel. */ @Composable -fun FileSelect(onFilesSelected: (ImmutableList) -> Unit = {}) { - val hasLaunched by remember { mutableStateOf(AtomicBoolean(false)) } - val resolver = LocalContext.current.contentResolver +expect fun FileSelect(onFilesSelected: (ImmutableList) -> Unit = {}) - val launcher = - rememberLauncherForActivityResult( - contract = ActivityResultContracts.OpenMultipleDocuments(), - onResult = { uris: List -> - onFilesSelected( - uris - .map { - SelectedMedia(it, resolver.getType(it)) - }.toImmutableList(), - ) - hasLaunched.set(false) - }, - ) - - @Composable - fun LaunchFilePicker() { - SideEffect { - if (!hasLaunched.getAndSet(true)) { - launcher.launch( - arrayOf( - "audio/*", - "application/pdf", - ), - ) - } - } - } - - LaunchFilePicker() -} +/** Opens the platform's document picker for one file of [mimeTypes]; reports null on cancel. */ +@Composable +expect fun DocumentSelectSingle( + mimeTypes: List, + onPicked: (SelectedMedia?) -> Unit, +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromGallery.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SelectFromGallery.kt similarity index 50% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromGallery.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SelectFromGallery.kt index 773711d248..f7e0dea30e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromGallery.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SelectFromGallery.kt @@ -18,77 +18,26 @@ * 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.actions.uploads +package com.vitorpamplona.amethyst.commons.ui.actions.uploads -import android.net.Uri -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.PickVisualMediaRequest -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.height import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.upload_image +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.service.uploads.isAvif import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList -import java.util.concurrent.atomic.AtomicBoolean - -@Stable -class SelectedMedia( - val uri: Uri, - val mimeType: String?, -) { - fun isImage() = mimeType?.startsWith("image") - - fun isGif() = mimeType?.equals("image/gif", ignoreCase = true) == true - - fun isVideo() = mimeType?.startsWith("video") - - fun isAudio() = mimeType?.startsWith("audio") - - fun isNotMedia() = - mimeType?.let { - !(it.startsWith("image") || it.startsWith("video") || it.startsWith("audio")) - } ?: true - - fun isDocument() = mimeType == "application/pdf" - - /** - * Returns true if [MediaCompressor.compress] would actually compress this file when a - * non-UNCOMPRESSED quality is selected. AVIF, GIF, SVG, and unknown MIME types pass - * through MediaCompressor unchanged — the compression-quality slider has no effect on - * them, so the UI hides the slider when no selected files are compressible. - * - * Keep this in sync with the branching in MediaCompressor.compress() — if either side - * drifts, the UI will lie to the user. - */ - fun isCompressible(): Boolean { - val mt = mimeType?.lowercase() ?: return false - return when { - mt.startsWith("video") -> true - mt.startsWith("image") -> - !mt.contains("gif") && - !mt.contains("svg") && - !isAvif(mt) - else -> false - } - } -} @Composable fun SelectFromGallery( @@ -161,64 +110,13 @@ private fun GallerySelectButton( } } +/** Opens the platform's photo/video picker (up to 10 items); reports an empty list on cancel. */ @Composable -fun GallerySelect(onImageUri: (ImmutableList) -> Unit = {}) { - val hasLaunched by remember { mutableStateOf(AtomicBoolean(false)) } - val resolver = LocalContext.current.contentResolver - - val launcher = - rememberLauncherForActivityResult( - contract = ActivityResultContracts.PickMultipleVisualMedia(10), - onResult = { uris: List -> - onImageUri( - uris - .map { - SelectedMedia(it, resolver.getType(it)) - }.toImmutableList(), - ) - hasLaunched.set(false) - }, - ) - - @Composable - fun LaunchGallery() { - SideEffect { - if (!hasLaunched.getAndSet(true)) { - launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo)) - } - } - } - - LaunchGallery() -} +expect fun GallerySelect(onImageUri: (ImmutableList) -> Unit = {}) +/** Opens the platform's photo/video picker for one item ([imagesOnly]: photos only); reports null on cancel. */ @Composable -fun GallerySelectSingle(onImageUri: (SelectedMedia?) -> Unit = {}) { - val hasLaunched by remember { mutableStateOf(AtomicBoolean(false)) } - val resolver = LocalContext.current.contentResolver - - val launcher = - rememberLauncherForActivityResult( - contract = ActivityResultContracts.PickVisualMedia(), - onResult = { uri: Uri? -> - if (uri != null) { - onImageUri(SelectedMedia(uri, resolver.getType(uri))) - } else { - onImageUri(null) - } - - hasLaunched.set(false) - }, - ) - - @Composable - fun LaunchGallery() { - SideEffect { - if (!hasLaunched.getAndSet(true)) { - launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo)) - } - } - } - - LaunchGallery() -} +expect fun GallerySelectSingle( + imagesOnly: Boolean = false, + onImageUri: (SelectedMedia?) -> Unit = {}, +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/ShowImageUploadItem.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/ShowImageUploadItem.kt similarity index 83% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/ShowImageUploadItem.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/ShowImageUploadItem.kt index d508587166..70306318cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/ShowImageUploadItem.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/ShowImageUploadItem.kt @@ -18,13 +18,8 @@ * 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.actions.uploads +package com.vitorpamplona.amethyst.commons.ui.actions.uploads -import android.content.Context -import android.graphics.Bitmap -import android.media.MediaMetadataRetriever -import android.net.Uri -import android.os.Build import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -47,7 +42,6 @@ import androidx.compose.material3.ProgressIndicatorDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -56,9 +50,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -78,21 +70,21 @@ import com.vitorpamplona.amethyst.commons.resources.uploading_state_hashing import com.vitorpamplona.amethyst.commons.resources.uploading_state_ready import com.vitorpamplona.amethyst.commons.resources.uploading_state_server_processing import com.vitorpamplona.amethyst.commons.resources.uploading_state_uploading +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMediaProcessing +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberVideoThumbnail import com.vitorpamplona.amethyst.commons.ui.components.AutoNonlazyGrid import com.vitorpamplona.amethyst.commons.ui.note.CloseIcon +import com.vitorpamplona.amethyst.commons.ui.note.platform.VideoView import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size40Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.playback.composable.VideoView -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState -import com.vitorpamplona.quartz.utils.Log -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @Composable fun ShowImageUploadGallery( @@ -105,23 +97,6 @@ fun ShowImageUploadGallery( } } -/** - * Creates a bitmap thumbnail from video uri of the scheme type content:// - */ -fun createVideoThumb( - context: Context, - uri: Uri, -): Bitmap? { - try { - val mediaMetadataRetriever = MediaMetadataRetriever() - mediaMetadataRetriever.setDataSource(context, uri) - return mediaMetadataRetriever.frameAtTime - } catch (ex: Exception) { - Log.w("NewPostView", "Couldn't create thumbnail, but the video can be uploaded", ex) - } - return null -} - @Composable fun ShowImageGallery( media: SelectedMedia, @@ -154,30 +129,15 @@ fun ShowImageGallery( ) } - media.isVideo() == true && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> { - var bitmap by remember { mutableStateOf(null) } - val context = LocalContext.current - - LaunchedEffect(key1 = media) { - launch(Dispatchers.IO) { - try { - bitmap = createVideoThumb(context, media.uri) - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.w("NewPostView", "Couldn't create thumbnail, but the video can be uploaded", e) - } - } - } - - if (bitmap != null) { - bitmap?.let { - Image( - bitmap = it.asImageBitmap(), - contentDescription = "some useful description", - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxWidth(), - ) - } + media.isVideo() == true -> { + val thumbnail = rememberVideoThumbnail(media) + if (thumbnail != null) { + Image( + bitmap = thumbnail, + contentDescription = "some useful description", + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxWidth(), + ) } else { VideoView( videoUri = media.uri.toString(), diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VideoThumbnail.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VideoThumbnail.kt new file mode 100644 index 0000000000..c857778e9b --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VideoThumbnail.kt @@ -0,0 +1,32 @@ +/* + * 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.commons.ui.actions.uploads + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ImageBitmap +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia + +/** + * A frame of the picked video [media], for the upload preview. Null while it decodes, and where + * the platform cannot decode one; the preview then plays the video itself. + */ +@Composable +expect fun rememberVideoThumbnail(media: SelectedMedia): ImageBitmap? diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.kt new file mode 100644 index 0000000000..eeabee2c7a --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.kt @@ -0,0 +1,30 @@ +/* + * 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.commons.ui.components + +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri + +/** + * Accepts images pasted or dragged into the field (keyboard GIFs, stickers, clipboard images), + * reporting each one's address and MIME type. Android only for now; elsewhere it adds nothing. + */ +expect fun Modifier.imageContentReceiver(onImage: (MediaUri, String?) -> Unit): Modifier diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/PlatformBackHandler.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/PlatformBackHandler.kt new file mode 100644 index 0000000000..60225f29ed --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/PlatformBackHandler.kt @@ -0,0 +1,33 @@ +/* + * 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.commons.ui.components + +import androidx.compose.runtime.Composable + +/** + * Intercepts the system back gesture/button while [enabled]: AndroidX's `BackHandler` on + * Android, Compose Multiplatform's on desktop (Esc) and iOS (swipe back). + */ +@Composable +expect fun PlatformBackHandler( + enabled: Boolean = true, + onBack: () -> Unit, +) diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/RememberViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/RememberViewModel.kt index 5902d14fb1..5542f13703 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/RememberViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/RememberViewModel.kt @@ -21,8 +21,12 @@ package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras import kotlin.reflect.KClass /** @@ -56,3 +60,22 @@ inline fun rememberViewModel( key: String? = null, noinline factory: () -> VM, ): VM = rememberViewModel(VM::class, key, factory) + +/** + * One [ViewModelProvider.Factory] per call site for the lambda overloads of [rememberViewModel], + * instead of a new object on every recomposition. It calls the latest [factory], which only runs + * when the store has no view model yet. + */ +@Composable +internal fun rememberLambdaFactory(factory: () -> VM): ViewModelProvider.Factory { + val currentFactory by rememberUpdatedState(factory) + return remember { + object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = currentFactory() as T + } + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/SetDialogToEdgeToEdge.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/SetDialogToEdgeToEdge.kt new file mode 100644 index 0000000000..e35401e40f --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/SetDialogToEdgeToEdge.kt @@ -0,0 +1,40 @@ +/* + * 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.commons.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.window.DialogProperties + +/** + * Stretches the enclosing dialog's window over the whole activity and matches its system-bar + * icon colours to the theme. Android only; other platforms' dialogs need no such fix. + */ +@Composable +expect fun SetDialogToEdgeToEdge() + +/** + * Properties for a full-window dialog drawn edge to edge (pair with [SetDialogToEdgeToEdge]). On + * Android the dialog window also stops fitting the system bars. + */ +expect fun edgeToEdgeDialogProperties( + dismissOnBackPress: Boolean = true, + dismissOnClickOutside: Boolean = true, +): DialogProperties diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.kt index afcf42777d..8a5766f8bd 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.kt @@ -33,3 +33,7 @@ fun interface ShortNotice { */ @Composable expect fun rememberShortNotice(): ShortNotice + +/** Like [rememberShortNotice], shown longer: for errors the reader needs time to take in. */ +@Composable +expect fun rememberLongNotice(): ShortNotice diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ZapButtonConfig.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ZapButtonConfig.kt new file mode 100644 index 0000000000..340404608d --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ZapButtonConfig.kt @@ -0,0 +1,52 @@ +/* + * 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.commons.ui.components + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import com.vitorpamplona.amethyst.commons.ui.theme.Size14Modifier +import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier +import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp + +/** + * Configuration for zap button behavior and appearance. Never mutated after construction, so it + * is marked immutable for Compose (its `Modifier` and `List` fields would otherwise make it unstable). + */ +@Immutable +data class ZapButtonConfig( + val grayTint: Color = Color.Gray, + val iconSize: Dp = Size35dp, + val iconSizeModifier: Modifier = Size20Modifier, + val animationModifier: Modifier = Size14Modifier, + val showUserFinderSubscription: Boolean = false, + val zapAmountChoices: List? = null, + val thankYouText: String? = null, + val buttonText: String? = null, +) + +/** + * Callbacks for zap button events + */ +data class ZapButtonCallbacks( + val onZapComplete: ((Boolean) -> Unit)? = null, +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/namecoin/NamecoinResolutionRow.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/namecoin/NamecoinResolutionRow.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/namecoin/NamecoinResolutionRow.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/namecoin/NamecoinResolutionRow.kt index c49bffebb1..766c0e95eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/namecoin/NamecoinResolutionRow.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/namecoin/NamecoinResolutionRow.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.components.namecoin +package com.vitorpamplona.amethyst.commons.ui.components.namecoin import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -45,16 +45,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.namecoin.NamecoinResolveState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.note.UserPicture +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinResolveOutcome import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.delay import kotlinx.coroutines.withContext @@ -162,6 +163,7 @@ fun NamecoinResolutionRow( if (!looksLikeNamecoinIdentifier(trimmed)) return var state by remember { mutableStateOf(null) } + val resolver = LocalAppServices.current.namecoinResolver LaunchedEffect(trimmed) { // Match UserSuggestionState's 300 ms debounce so we don't fire a @@ -171,7 +173,7 @@ fun NamecoinResolutionRow( val outcome = withContext(Dispatchers.IO) { runCatching { - Amethyst.instance.namecoinResolver.resolveDetailed(trimmed) + resolver.resolveDetailed(trimmed) }.getOrElse { NamecoinResolveOutcome.ServersUnreachable( it.message ?: it::class.simpleName ?: "Lookup error", diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/wavefront/FakeWaveformAnimation.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/wavefront/FakeWaveformAnimation.kt new file mode 100644 index 0000000000..b445c7c879 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/wavefront/FakeWaveformAnimation.kt @@ -0,0 +1,100 @@ +/* + * 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.commons.ui.components.wavefront + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableFloatState +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn +import kotlin.math.PI +import kotlin.math.sin + +@Preview +@Composable +fun FakeWaveformAnimationPreview() { + val state = + remember { + mutableFloatStateOf((500 % 1000.0f) / 1000.0f) + } + ThemeComparisonColumn { + FakeWaveformAnimation(state, 50, Modifier.width(200.dp)) + } +} + +@Composable +fun FakeWaveformAnimation( + waveformProgress: MutableFloatState, + barCount: Int = 50, + modifier: Modifier = Modifier, +) { + val barBrush = + Brush.linearGradient( + colors = listOf(Color(0xff2598cf), Color(0xff652d80)), + start = Offset(0.2f * 128f, 0f), + end = Offset(0.2f * 128f + 128f, 0f), + ) + + Canvas( + modifier = + modifier + .fillMaxWidth() + .requiredHeight(48.dp) + .padding(horizontal = 10.dp), + ) { + val barWidth = 3.dp.toPx() + val barPadding = 2.dp.toPx() + val barRadius = 2.dp.toPx() + val totalBarWidth = barWidth + barPadding + val startX = (size.width - (barCount * totalBarWidth - barPadding)) / 2f + + for (i in 0 until barCount) { + val phase = (i.toFloat() / barCount) * 2f * PI.toFloat() + val wave = sin(waveformProgress.floatValue * 2f * PI.toFloat() + phase) + val barFraction = ((wave + 1f) / 2f) + val barHeight = size.height * barFraction + val x = startX + i * totalBarWidth + val y = (size.height - barHeight) / 2f + + drawRoundRect( + brush = barBrush, + topLeft = Offset(x, y), + size = Size(barWidth, barHeight), + cornerRadius = CornerRadius(barRadius, barRadius), + style = Fill, + ) + } + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/QueryParameters.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/QueryParameters.kt new file mode 100644 index 0000000000..1faf16cf74 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/QueryParameters.kt @@ -0,0 +1,53 @@ +/* + * 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.commons.ui.navigation + +/** + * The value of [parameterName] in this URI's query string, without going through + * `java.net.URI`. + * + * Needed because `java.net.URI` only exposes `rawQuery` for *hierarchical* URIs. A URI + * with a scheme and no `//` is **opaque** — everything after the colon is one + * scheme-specific part — so `URI("marmot:?account=npub1…").rawQuery` is null. That + * is the shape `NotificationRoutes.marmotUri` + * produces, so every Marmot group notification silently lost its `?account=` and opened + * the group under whichever account happened to be current instead of switching first. + * + * Splitting on the first `?` gets the same answer for both shapes, and returns null for a + * bare `nevent1…` with no query at all. A `#fragment` is dropped; a value may itself contain `=`. + * + * `UriParser` reads an opaque query correctly too, and is the + * right tool when a URI is already known to be well-formed. It is not this one: it builds a + * `java.net.URI`, which *throws* on anything that is not a legal URI. What arrives here comes + * from an exported, browsable scheme, so it can be any string at all, and every caller on the + * deep-link path treats an unreadable uri as "no route" rather than as a crash. + */ +fun String.findQueryParameterValue(parameterName: String): String? { + // A raw `#` starts the fragment, which is not part of the query (as `java.net.URI` read it). + val query = substringBefore('#').substringAfter('?', "") + if (query.isEmpty()) return null + + return query + .split('&') + .firstOrNull { it.substringBefore('=') == parameterName } + ?.substringAfter('=', "") + ?.ifEmpty { null } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/bottombars/AppBottomBar.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/bottombars/AppBottomBar.kt index 6b17157a0b..e60a9b8150 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/bottombars/AppBottomBar.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.navigation.bottombars +package com.vitorpamplona.amethyst.commons.ui.navigation.bottombars import androidx.compose.foundation.Canvas import androidx.compose.foundation.background @@ -43,7 +43,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.browser.OmniboxInput import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppIcon @@ -55,6 +54,8 @@ import com.vitorpamplona.amethyst.commons.ui.insets.keyboardAsState import com.vitorpamplona.amethyst.commons.ui.layouts.LocalScreenLayout import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.LocalTabReselectCoordinator import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.platform.rememberNappletIconModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding @@ -64,7 +65,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size25Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size27Modifier import com.vitorpamplona.amethyst.commons.ui.theme.onSurface65 import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.favorites.rememberNappletIconModel /** Content height of the [AppBottomBar] (the 50.dp Column inside [RenderBottomMenu]), * exclusive of the system navigation-bar inset. Used by FAB callers that want to @@ -114,7 +114,7 @@ fun AppBottomBar( // Favorite entries in the unified list resolve to a live favorite for their icon/label and to an // embedded-tab route. Both kinds embed in-process (WebApp → browser surface, NostrApp → napplet // surface), so such a tab swaps in place rather than launching an activity from the bottom row. - val favorites by Amethyst.instance.favoriteApps.favorites + val favorites by LocalAppServices.current.favoriteApps.favorites .collectAsStateWithLifecycle() val isKeyboardState by keyboardAsState() @@ -134,10 +134,11 @@ internal fun rememberFavoriteIconModel(fav: FavoriteApp): Any? = when (fav) { is FavoriteApp.WebApp -> { // Captured favicons, keyed so the icon appears once the site's capture lands. - val iconKeys by Amethyst.instance.browserIcons.keys + val browserIcons = LocalAppServices.current.browserIcons + val iconKeys by browserIcons.keys .collectAsStateWithLifecycle() remember(fav, iconKeys) { - OmniboxInput.hostOf(fav.url)?.let(Amethyst.instance.browserIcons::iconModelFor) + OmniboxInput.hostOf(fav.url)?.let(browserIcons::iconModelFor) } } @@ -217,7 +218,7 @@ class BottomBarSlot( /** Resolves an entry to its live [BottomBarSlot], or null if it no longer resolves (deleted favorite, etc.). */ @Composable -internal fun rememberBottomBarSlot( +fun rememberBottomBarSlot( entry: BottomBarEntry, favoritesById: Map, accountViewModel: AccountViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/GroupBottomBarEntries.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/bottombars/GroupBottomBarEntries.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/GroupBottomBarEntries.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/bottombars/GroupBottomBarEntries.kt index 570e5518e9..7f79132f37 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/GroupBottomBarEntries.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/bottombars/GroupBottomBarEntries.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.navigation.bottombars +package com.vitorpamplona.amethyst.commons.ui.navigation.bottombars import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -44,10 +44,10 @@ import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.channel.Channel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.concord_home_title import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage +import com.vitorpamplona.amethyst.commons.ui.platform.rememberConcordImageModel import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.rememberConcordImageModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nip29RelayGroups.GroupId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/NavBarItem.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/bottombars/NavBarItem.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/NavBarItem.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/bottombars/NavBarItem.kt index 5182e8e66c..34e54a76e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/NavBarItem.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/bottombars/NavBarItem.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.navigation.bottombars +package com.vitorpamplona.amethyst.commons.ui.navigation.bottombars import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerSectionVisibility.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/drawer/DrawerSectionVisibility.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerSectionVisibility.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/drawer/DrawerSectionVisibility.kt index 438305ddcc..f50be35832 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerSectionVisibility.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/drawer/DrawerSectionVisibility.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.navigation.drawer +package com.vitorpamplona.amethyst.commons.ui.navigation.drawer import com.vitorpamplona.amethyst.commons.model.navigation.DrawerItemVisibility import com.vitorpamplona.amethyst.commons.model.navigation.MandatoryDrawerItems diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerSections.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/drawer/DrawerSections.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerSections.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/drawer/DrawerSections.kt index 0e0b1353f8..81f99e9299 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerSections.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/drawer/DrawerSections.kt @@ -18,9 +18,8 @@ * 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.navigation.drawer +package com.vitorpamplona.amethyst.commons.ui.navigation.drawer -import android.os.Build import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -33,7 +32,8 @@ import com.vitorpamplona.amethyst.commons.resources.drawer_section_feeds import com.vitorpamplona.amethyst.commons.resources.drawer_section_navigate import com.vitorpamplona.amethyst.commons.resources.drawer_section_system import com.vitorpamplona.amethyst.commons.resources.drawer_section_you -import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarCatalog +import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.NavBarCatalog +import com.vitorpamplona.amethyst.commons.ui.platform.supportsEmbeddedAppTabs import org.jetbrains.compose.resources.StringResource /** @@ -126,9 +126,8 @@ private val DrawerFeedsItems: List = NavBarItem.CALENDARS, NavBarItem.CALENDAR_COLLECTIONS, NavBarItem.SOFTWARE_APPS, - // Favorites can be pinned as inline tabs that render on a cross-process surface - // (SurfaceControlViewHost), which needs API 30+. Gate the whole grid on R+ for that reason. - NavBarItem.FAVORITE_APPS.takeIf { Build.VERSION.SDK_INT >= Build.VERSION_CODES.R }, + // Favorites can be pinned as inline tabs, which need a platform surface for them. + NavBarItem.FAVORITE_APPS.takeIf { supportsEmbeddedAppTabs }, NavBarItem.NAPPLETS, NavBarItem.NSITES, NavBarItem.FOLLOW_PACKS, diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/MonthDayTime.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/MonthDayTime.kt new file mode 100644 index 0000000000..5eb37d3c10 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/MonthDayTime.kt @@ -0,0 +1,32 @@ +/* + * 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.commons.ui.note + +import androidx.compose.runtime.Composable + +/** + * Formats an instant (epoch seconds) as the locale's month, day and short time in one pattern + * ("May 28, 14:32" / "28 May, 2:32 PM"), in the current time zone. Android follows the system + * 12/24-hour setting; the JVM and iOS follow the locale. Identity-stable, so it can key a + * `remember`. + */ +@Composable +expect fun rememberMonthDayTimeFormatter(): (epochSeconds: Long) -> String diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt index 94711c83d9..4b88cbbbff 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt @@ -25,8 +25,8 @@ import androidx.compose.runtime.Composable /** * Formats instants with a Unicode LDML date skeleton ("yMMMd", "MMMd", "yMMM"), letting the * platform pick the default locale's field order ("MMM d, y" in en-US, "d MMM y" in en-GB). - * Safe to call from any thread. Rebuilds itself when the default locale changes (the JVM actual - * also on a time-zone change; Android and iOS keep the zone they were built with). + * Safe to call from any thread. Rebuilds itself when the default locale or time zone changes (iOS + * keeps the zone it was built with). */ expect class DateSkeletonFormatter( skeleton: String, diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatform.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatform.kt index ab6e88a774..367dd82ee0 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatform.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatform.kt @@ -36,6 +36,8 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewState +import com.vitorpamplona.amethyst.commons.ui.components.ZapButtonCallbacks +import com.vitorpamplona.amethyst.commons.ui.components.ZapButtonConfig import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.CalendarRsvpCard import com.vitorpamplona.amethyst.commons.ui.note.types.EditState @@ -398,6 +400,16 @@ interface NotePlatform { nav: INav, ) {} + /** A labelled zap button for [baseNote]'s author, outside a reactions row (DVMs, donations). */ + @Composable + fun ReusableZapButton( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, + config: ZapButtonConfig, + callbacks: ZapButtonCallbacks, + ) {} + /** The editor for a new version of [edit], shown while [versionLookingAt] is on screen. */ @Composable fun EditPostView( diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatformCalls.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatformCalls.kt index 00b25ef085..30f946780d 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatformCalls.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/platform/NotePlatformCalls.kt @@ -35,6 +35,8 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.ui.components.GenericLoadable import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewState +import com.vitorpamplona.amethyst.commons.ui.components.ZapButtonCallbacks +import com.vitorpamplona.amethyst.commons.ui.components.ZapButtonConfig import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.types.EditState import com.vitorpamplona.amethyst.commons.ui.theme.Font14SP @@ -534,6 +536,15 @@ fun ZapReaction( nav = nav, ) +@Composable +fun ReusableZapButton( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, + config: ZapButtonConfig = ZapButtonConfig(), + callbacks: ZapButtonCallbacks = ZapButtonCallbacks(), +) = LocalNotePlatform.current.ReusableZapButton(baseNote, accountViewModel, nav, config, callbacks) + @Composable fun EditPostView( onClose: () -> Unit, diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/AppPlatform.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/AppPlatform.kt index 4fd7c0991c..830d968259 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/AppPlatform.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/AppPlatform.kt @@ -24,9 +24,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.UriHandler +import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.ScanOutcome +import com.vitorpamplona.amethyst.commons.ui.settings.SettingsCategory import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.quartz.concord.cord02Community.ImagePointer /** * The app shell's pieces that shared screens embed but only the front end can draw yet: the @@ -36,6 +41,12 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel */ @Stable interface AppPlatform { + /** Whether this build can cast media to a TV (the Play flavour's Cast SDK). */ + val isCastingAvailable: Boolean get() = false + + /** The settings screen's legal links, when the build's distribution calls for them. */ + fun legalSettingsCategory(uriHandler: UriHandler): SettingsCategory? = null + @Composable fun AppBottomBar( selectedRoute: Route?, @@ -64,6 +75,42 @@ interface AppPlatform { modifier: Modifier, ) {} + /** The camera QR scanner, full screen. [onScan] says whether the payload was used. */ + @Composable + fun QrCodeScannerDialog( + onDismiss: () -> Unit, + onScan: (String) -> ScanOutcome, + ) {} + + /** Opens favorite apps and web links in the platform's app surface. */ + @Composable + fun rememberAppLauncher(): AppLauncher = AppLauncher.None + + /** + * The image model for a CORD-02 community picture: the plain URL for a url-only pointer, or a + * local copy of the decrypted blob for an encrypted one (null while it loads or if it fails). + */ + @Composable + fun rememberConcordImageModel( + pointer: ImagePointer?, + accountViewModel: AccountViewModel, + ): String? = pointer?.takeIf { !it.isResolvable() }?.url?.ifBlank { null } + + /** The cached icon of the napplet or nsite at [coordinate], if one was fetched. */ + @Composable + fun rememberNappletIconModel(coordinate: String): String? = null + + /** The icon a napplet manifest by [author] / [identifier] declares, if cached. */ + @Composable + fun rememberManifestIconModel( + author: String, + identifier: String, + ): String? = null + + /** The favicon the browser captured for [url]'s host, if any. */ + @Composable + fun rememberWebAppIconModel(url: String): String? = null + /** Draws nothing: previews, and front ends still wiring their pieces. */ object None : AppPlatform } @@ -96,3 +143,61 @@ fun GeohashLocationPickerContent( onConfirm: (String) -> Unit, modifier: Modifier = Modifier, ) = LocalAppPlatform.current.GeohashLocationPickerContent(initialGeohash, confirmLabel, onConfirm, modifier) + +@Composable +fun QrCodeScannerDialog( + onDismiss: () -> Unit, + onScan: (String) -> ScanOutcome, +) = LocalAppPlatform.current.QrCodeScannerDialog(onDismiss, onScan) + +@Composable +fun rememberAppLauncher(): AppLauncher = LocalAppPlatform.current.rememberAppLauncher() + +@Composable +fun rememberConcordImageModel( + pointer: ImagePointer?, + accountViewModel: AccountViewModel, +): String? = LocalAppPlatform.current.rememberConcordImageModel(pointer, accountViewModel) + +@Composable +fun rememberNappletIconModel(coordinate: String): String? = LocalAppPlatform.current.rememberNappletIconModel(coordinate) + +@Composable +fun rememberManifestIconModel( + author: String, + identifier: String, +): String? = LocalAppPlatform.current.rememberManifestIconModel(author, identifier) + +@Composable +fun rememberWebAppIconModel(url: String): String? = LocalAppPlatform.current.rememberWebAppIconModel(url) + +/** + * Opens a favorite app or a web link in the platform's app surface (on Android, the sandboxed + * full-screen WebView in its own task). + */ +interface AppLauncher { + /** [stillLoading] is shown when a Nostr app's defining event has not arrived yet. */ + fun launch( + app: FavoriteApp, + stillLoading: String, + ) + + /** [preferTor] forces Tor when available, e.g. for `.onion` hosts. */ + fun launchUrl( + url: String, + preferTor: Boolean = false, + ) + + /** Launches nothing: front ends without an app surface yet. */ + object None : AppLauncher { + override fun launch( + app: FavoriteApp, + stillLoading: String, + ) {} + + override fun launchUrl( + url: String, + preferTor: Boolean, + ) {} + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/EmbeddedAppSurfaces.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/EmbeddedAppSurfaces.kt new file mode 100644 index 0000000000..031114b884 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/EmbeddedAppSurfaces.kt @@ -0,0 +1,27 @@ +/* + * 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.commons.ui.platform + +/** + * Whether favorite apps can be pinned as inline tabs. On Android they render on a cross-process + * surface (SurfaceControlViewHost), which needs API 30; other front ends have no such surface yet. + */ +expect val supportsEmbeddedAppTabs: Boolean diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/LocalAppServices.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/LocalAppServices.kt new file mode 100644 index 0000000000..b691c37bf7 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/LocalAppServices.kt @@ -0,0 +1,30 @@ +/* + * 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.commons.ui.platform + +import androidx.compose.runtime.staticCompositionLocalOf +import com.vitorpamplona.amethyst.commons.service.AppServices + +/** + * The front end's [AppServices], installed at the composition root. No default: a screen that + * reads it outside an app root is a wiring bug, not something to paper over with empty stores. + */ +val LocalAppServices = staticCompositionLocalOf { error("No AppServices installed at the composition root") } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/UserFeedViewModel.kt similarity index 91% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/UserFeedViewModel.kt index 953e7058cf..f176b4e12f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/UserFeedViewModel.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 +package com.vitorpamplona.amethyst.commons.ui.screen import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable @@ -32,11 +32,11 @@ import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.service.BundledUpdate import com.vitorpamplona.amethyst.commons.util.equalImmutableLists -import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.utils.Log import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch @@ -56,8 +56,6 @@ open class UserFeedViewModel( override val isRefreshing: MutableState = mutableStateOf(false) private fun refreshSuspended() { - checkNotInMainThread() - try { isRefreshing.value = true @@ -100,24 +98,24 @@ open class UserFeedViewModel( } init { - Log.d("Init") { "${this.javaClass.simpleName}" } + Log.d("Init") { "${this::class.simpleName}" } viewModelScope.launch(Dispatchers.IO) { LocalCache.live.newEventBundles.collect { newNotes -> - Log.d("Rendering Metrics") { "Update feeds: ${this@UserFeedViewModel.javaClass.simpleName} with ${newNotes.size}" } + Log.d("Rendering Metrics") { "Update feeds: ${this@UserFeedViewModel::class.simpleName} with ${newNotes.size}" } invalidateData() } } viewModelScope.launch(Dispatchers.IO) { LocalCache.live.deletedEventBundles.collect { newNotes -> - Log.d("Rendering Metrics") { "Delete from feeds: ${this@UserFeedViewModel.javaClass.simpleName} with ${newNotes.size}" } + Log.d("Rendering Metrics") { "Delete from feeds: ${this@UserFeedViewModel::class.simpleName} with ${newNotes.size}" } invalidateData() } } } override fun onCleared() { - Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" } + Log.d("Init") { "OnCleared: ${this::class.simpleName}" } bundler.cancel() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesFeedLoaded.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/articles/ArticlesFeedLoaded.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesFeedLoaded.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/articles/ArticlesFeedLoaded.kt index 7ead0106ea..d0e59db432 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesFeedLoaded.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/articles/ArticlesFeedLoaded.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.articles +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.articles import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -33,10 +33,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.ChannelCardCompose import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.ChannelCardCompose import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/articles/ArticlesScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/articles/ArticlesScreen.kt index 45c41fe5e5..52bdd4c631 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/articles/ArticlesScreen.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.articles +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.articles import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -36,8 +36,8 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.articles.datasource.ArticlesFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.articles.datasource.ArticlesFilterAssemblerSubscription @Composable fun ArticlesScreen( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesTopBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/articles/ArticlesTopBar.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesTopBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/articles/ArticlesTopBar.kt index 8d1ef3f756..b9a36fd5ba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesTopBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/articles/ArticlesTopBar.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.articles +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.articles import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/datasource/ArticlesFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/articles/datasource/ArticlesFilterAssemblerSubscription.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/datasource/ArticlesFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/articles/datasource/ArticlesFilterAssemblerSubscription.kt index 9ca1720f0a..2d4e86b988 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/datasource/ArticlesFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/articles/datasource/ArticlesFilterAssemblerSubscription.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.articles.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.articles.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictEventViews.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/backups/BackupConflictEventViews.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictEventViews.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/backups/BackupConflictEventViews.kt index a081aa07f3..276a3ecb74 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictEventViews.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/backups/BackupConflictEventViews.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.backups +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -142,13 +142,13 @@ import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.commons.ui.note.elements.BannerImage +import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.LoadRelayGroupChannel +import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.pluralStringRes -import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.diff.ContentChange import com.vitorpamplona.quartz.nip01Core.diff.ValueChange diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/backups/BackupConflictListViews.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/backups/BackupConflictListViews.kt index eba5ffc5f3..e9e10702f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/backups/BackupConflictListViews.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.backups +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -117,12 +117,12 @@ import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.commons.ui.note.LoadPublicChatChannel import com.vitorpamplona.amethyst.commons.ui.note.LoadUser import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListDiff import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListDiff diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/backups/BackupConflictPresentation.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/backups/BackupConflictPresentation.kt index 9980be0f5d..c08ad14e52 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/backups/BackupConflictPresentation.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.backups +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable @@ -114,7 +114,7 @@ import com.vitorpamplona.amethyst.commons.resources.backup_type_trust_providers import com.vitorpamplona.amethyst.commons.resources.backup_type_trust_providers_explainer import com.vitorpamplona.amethyst.commons.resources.backup_type_trusted_relays import com.vitorpamplona.amethyst.commons.resources.backup_type_trusted_relays_explainer -import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListDiff import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListDiff @@ -341,7 +341,7 @@ fun rememberPresentation(diff: EventDiff): DiffPresentation { } /** Every label [presentationOf] resolves through its `str` parameter for [diff]. */ -internal fun labelsWrittenBy(diff: EventDiff): List = +fun labelsWrittenBy(diff: EventDiff): List = when (diff) { is MetadataDiff -> PROFILE_FIELD_LABELS is AdvertisedRelayListDiff -> RELAY_MARKER_LABELS diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt index 30c307d28f..864a22bb1c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/backups/BackupConflictReviewScreen.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.backups +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictVisuals.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/backups/BackupConflictVisuals.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictVisuals.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/backups/BackupConflictVisuals.kt index 1d8e89a034..d3cc532872 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictVisuals.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/backups/BackupConflictVisuals.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.backups +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/BadgesScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/BadgesScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/BadgesScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/BadgesScreen.kt index 654cac5a9a..f62a2127e5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/BadgesScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/BadgesScreen.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.badges +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -35,8 +35,8 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges.datasource.BadgesFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.datasource.BadgesFilterAssemblerSubscription @Composable fun BadgesScreen( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/BadgesTopBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/BadgesTopBar.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/BadgesTopBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/BadgesTopBar.kt index a98a85ad36..1c1bbe0d06 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/BadgesTopBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/BadgesTopBar.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.badges +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/NewBadgeButton.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/NewBadgeButton.kt similarity index 87% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/NewBadgeButton.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/NewBadgeButton.kt index 2703e96f57..6b402c46ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/NewBadgeButton.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/NewBadgeButton.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.badges +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.FloatingActionButton @@ -29,22 +29,22 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.ic_compose import com.vitorpamplona.amethyst.commons.resources.new_badge +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.painterRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges.post.NewBadgeDialog +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges.post.NewBadgeModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size26Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.post.NewBadgeDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.post.NewBadgeModel @Composable fun NewBadgeButton(accountViewModel: AccountViewModel) { var showDialog by remember { mutableStateOf(false) } - val postViewModel: NewBadgeModel = viewModel() + val postViewModel: NewBadgeModel = rememberViewModel { NewBadgeModel() } if (showDialog) { NewBadgeDialog( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/award/AwardBadgeScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/award/AwardBadgeScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/award/AwardBadgeScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/award/AwardBadgeScreen.kt index bcd80f4559..7562ec5a8c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/award/AwardBadgeScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/award/AwardBadgeScreen.kt @@ -18,9 +18,8 @@ * 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.badges.award +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges.award -import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -47,7 +46,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.award_badge @@ -56,6 +54,8 @@ import com.vitorpamplona.amethyst.commons.resources.award_badge_remove_recipient import com.vitorpamplona.amethyst.commons.resources.award_badge_search_label import com.vitorpamplona.amethyst.commons.resources.award_badge_search_placeholder import com.vitorpamplona.amethyst.commons.ui.components.Nip05OrPubkeyLine +import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar @@ -75,7 +75,7 @@ fun AwardBadgeScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val vm: AwardBadgeViewModel = viewModel() + val vm: AwardBadgeViewModel = rememberViewModel { AwardBadgeViewModel() } LaunchedEffect(accountViewModel, kind, pubKeyHex, dTag) { vm.init(accountViewModel, kind, pubKeyHex, dTag) @@ -93,7 +93,7 @@ fun AwardBadgeScreen( onDispose { userSuggestions.reset() } } - BackHandler { + PlatformBackHandler { nav.popBack() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/award/AwardBadgeViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/award/AwardBadgeViewModel.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/award/AwardBadgeViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/award/AwardBadgeViewModel.kt index 4b118984b9..c4dee892d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/award/AwardBadgeViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/award/AwardBadgeViewModel.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.badges.award +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges.award import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/datasource/BadgesFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/datasource/BadgesFilterAssemblerSubscription.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/datasource/BadgesFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/datasource/BadgesFilterAssemblerSubscription.kt index 6bdee17125..2f8bb5a52c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/datasource/BadgesFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/datasource/BadgesFilterAssemblerSubscription.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.badges.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeDialog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/post/NewBadgeDialog.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeDialog.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/post/NewBadgeDialog.kt index dbb9476edb..774c5cc936 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeDialog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/post/NewBadgeDialog.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.badges.post +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges.post import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -54,14 +54,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.mediaServers.DEFAULT_MEDIA_SERVERS @@ -84,19 +82,20 @@ import com.vitorpamplona.amethyst.commons.resources.new_badge import com.vitorpamplona.amethyst.commons.resources.strip_metadata_description import com.vitorpamplona.amethyst.commons.resources.strip_metadata_label import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.ShowImageUploadGallery +import com.vitorpamplona.amethyst.commons.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer +import com.vitorpamplona.amethyst.commons.ui.components.edgeToEdgeDialogProperties import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.CreatingTopBar import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.SettingSwitchItem +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelect -import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery -import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -108,7 +107,6 @@ fun NewBadgeDialog( accountViewModel: AccountViewModel, ) { val account = accountViewModel.account - val context = LocalContext.current val scrollState = rememberScrollState() @@ -133,12 +131,7 @@ fun NewBadgeDialog( Dialog( onDismissRequest = onClose, - properties = - DialogProperties( - usePlatformDefaultWidth = false, - dismissOnClickOutside = false, - decorFitsSystemWindows = false, - ), + properties = edgeToEdgeDialogProperties(dismissOnClickOutside = false), ) { SetDialogToEdgeToEdge() Scaffold( @@ -152,7 +145,7 @@ fun NewBadgeDialog( }, onPost = { postViewModel.upload( - context, + accountViewModel.host.mediaUploader, onSuccess = onClose, onError = accountViewModel.toastManager::toast, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/post/NewBadgeModel.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/post/NewBadgeModel.kt index b73ecb2534..25f3ec1c03 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/badges/post/NewBadgeModel.kt @@ -18,9 +18,8 @@ * 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.badges.post +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges.post -import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -36,17 +35,21 @@ import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_de import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip58Badges.definition.tags.ThumbTag import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.launch -import java.util.UUID +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid /** * Drives the "new badge" creation flow: uploads the user-picked image (at the @@ -110,12 +113,12 @@ class NewBadgeModel : ViewModel() { name.isNotBlank() fun upload( - context: Context, + uploader: MediaUploader, onSuccess: () -> Unit, onError: (String, String) -> Unit, ) { try { - uploadUnsafe(context, onSuccess, onError) + uploadUnsafe(uploader, onSuccess, onError) } catch (e: SignerExceptions.ReadOnlyException) { viewModelScope.launch { onError( @@ -126,8 +129,9 @@ class NewBadgeModel : ViewModel() { } } + @OptIn(ExperimentalUuidApi::class) private fun uploadUnsafe( - context: Context, + uploader: MediaUploader, onSuccess: () -> Unit, onError: (String, String) -> Unit, ) { @@ -142,10 +146,10 @@ class NewBadgeModel : ViewModel() { orch.upload( alt = name, contentWarningReason = null, - mediaQuality = MediaCompressor.intToCompressorQuality(mediaQualitySlider), + mediaQuality = CompressorQuality.fromSlider(mediaQualitySlider), server = serverToUse, account = myAccount, - context = context, + uploader = uploader, useH265 = false, stripMetadata = stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, @@ -180,7 +184,7 @@ class NewBadgeModel : ViewModel() { val dimensions = uploaded.fileHeader.dim myAccount.sendBadgeDefinition( - badgeId = UUID.randomUUID().toString(), + badgeId = Uuid.random().toString(), name = name.trim(), imageUrl = imageUrl, imageDim = dimensions, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/OldBookmarkListScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/old/OldBookmarkListScreen.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/OldBookmarkListScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/old/OldBookmarkListScreen.kt index 6ddf7c23a6..c9b8137a4a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/OldBookmarkListScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/old/OldBookmarkListScreen.kt @@ -18,10 +18,8 @@ * 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.bookmarkgroups.old +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.old -import android.annotation.SuppressLint -import android.widget.Toast import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -45,9 +43,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.AddressableNote @@ -59,17 +55,19 @@ import com.vitorpamplona.amethyst.commons.resources.old_bookmarks_title import com.vitorpamplona.amethyst.commons.resources.private_bookmarks import com.vitorpamplona.amethyst.commons.resources.public_bookmarks import com.vitorpamplona.amethyst.commons.ui.components.DeletedItemsBanner +import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.screen.RefresheableFeedView +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.old.dal.OldBookmarkPrivateFeedViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.old.dal.OldBookmarkPublicFeedViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.TabRowHeight import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.old.dal.OldBookmarkPrivateFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.old.dal.OldBookmarkPublicFeedViewModel import kotlinx.coroutines.launch @Composable @@ -78,13 +76,13 @@ fun OldBookmarkListScreen( nav: INav, ) { val publicFeedViewModel: OldBookmarkPublicFeedViewModel = - viewModel( + rememberViewModel( key = "NostrOldBookmarkPublicFeedViewModel", factory = OldBookmarkPublicFeedViewModel.Factory(accountViewModel.account), ) val privateFeedViewModel: OldBookmarkPrivateFeedViewModel = - viewModel( + rememberViewModel( key = "NostrOldBookmarkPrivateFeedViewModel", factory = OldBookmarkPrivateFeedViewModel.Factory(accountViewModel.account), ) @@ -103,7 +101,7 @@ fun OldBookmarkListScreen( RenderOldBookmarkScreen(publicFeedViewModel, privateFeedViewModel, bookmarkState, accountViewModel, nav) } -@SuppressLint("LocalContextGetResourceValueCall") +@Suppress("LocalContextGetResourceValueCall") @Composable @OptIn(ExperimentalFoundationApi::class) private fun RenderOldBookmarkScreen( @@ -115,7 +113,7 @@ private fun RenderOldBookmarkScreen( ) { val pagerState = rememberPagerState { 2 } val coroutineScope = rememberCoroutineScope() - val context = LocalContext.current + val shortNotice = rememberShortNotice() val cache = accountViewModel.account.cache val deletedEventIds = remember(bookmarkState) { mutableSetOf() } @@ -178,12 +176,7 @@ private fun RenderOldBookmarkScreen( accountViewModel.launchSigner { accountViewModel.account.migrateOldBookmarksToNew() coroutineScope.launch { - Toast - .makeText( - context, - loadStringRes(Res.string.migrate_bookmarks_success), - Toast.LENGTH_SHORT, - ).show() + shortNotice.show(loadStringRes(Res.string.migrate_bookmarks_success)) } } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPrivateFeedFilter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPrivateFeedFilter.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPrivateFeedFilter.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPrivateFeedFilter.kt index 10732d7ac2..3483f70a41 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPrivateFeedFilter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPrivateFeedFilter.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.bookmarkgroups.old.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.old.dal import com.vitorpamplona.amethyst.commons.feeds.FeedFilter import com.vitorpamplona.amethyst.commons.model.Account diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPrivateFeedViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPrivateFeedViewModel.kt similarity index 82% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPrivateFeedViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPrivateFeedViewModel.kt index 21702425ae..7487023101 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPrivateFeedViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPrivateFeedViewModel.kt @@ -18,13 +18,15 @@ * 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.bookmarkgroups.old.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.old.dal import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel +import kotlin.reflect.KClass @Stable class OldBookmarkPrivateFeedViewModel( @@ -34,6 +36,9 @@ class OldBookmarkPrivateFeedViewModel( val account: Account, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = OldBookmarkPrivateFeedViewModel(account) as T + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = OldBookmarkPrivateFeedViewModel(account) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPublicFeedFilter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPublicFeedFilter.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPublicFeedFilter.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPublicFeedFilter.kt index cec82a37bb..865cfe8cfb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPublicFeedFilter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPublicFeedFilter.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.bookmarkgroups.old.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.old.dal import com.vitorpamplona.amethyst.commons.feeds.FeedFilter import com.vitorpamplona.amethyst.commons.model.Account diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPublicFeedViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPublicFeedViewModel.kt similarity index 82% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPublicFeedViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPublicFeedViewModel.kt index e0093152d9..7e63622092 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPublicFeedViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/bookmarkgroups/old/dal/OldBookmarkPublicFeedViewModel.kt @@ -18,13 +18,15 @@ * 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.bookmarkgroups.old.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.bookmarkgroups.old.dal import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel +import kotlin.reflect.KClass @Stable class OldBookmarkPublicFeedViewModel( @@ -34,6 +36,9 @@ class OldBookmarkPublicFeedViewModel( val account: Account, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = OldBookmarkPublicFeedViewModel(account) as T + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = OldBookmarkPublicFeedViewModel(account) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/browser/BrowserScreen.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/browser/BrowserScreen.kt index 725fd312a9..2df7df7c1a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/browser/BrowserScreen.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.browser +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.browser import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -63,7 +63,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction @@ -74,7 +73,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.browser.BrowserHistoryEntry import com.vitorpamplona.amethyst.commons.browser.DefaultWebClients import com.vitorpamplona.amethyst.commons.browser.OmniboxInput @@ -82,6 +80,8 @@ import com.vitorpamplona.amethyst.commons.browser.OmniboxSuggestions import com.vitorpamplona.amethyst.commons.browser.SuggestedWebApp import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppIcon +import com.vitorpamplona.amethyst.commons.favorites.PreloadFavoriteNostrApps +import com.vitorpamplona.amethyst.commons.favorites.favoriteCoordinateOf import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.icons.symbols.rememberMaterialSymbolPainter @@ -89,6 +89,7 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.browser_address_hint import com.vitorpamplona.amethyst.commons.resources.browser_clear import com.vitorpamplona.amethyst.commons.resources.browser_discover_napplets import com.vitorpamplona.amethyst.commons.resources.browser_discover_nsites @@ -104,21 +105,21 @@ import com.vitorpamplona.amethyst.commons.resources.favorite_app_still_loading import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.platform.rememberAppLauncher +import com.vitorpamplona.amethyst.commons.ui.platform.rememberNappletIconModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.favorites.favoriteAppItems +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.napplets.datasource.NappletsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.nsites.datasource.NsitesFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher -import com.vitorpamplona.amethyst.favorites.PreloadFavoriteNostrApps -import com.vitorpamplona.amethyst.favorites.rememberNappletIconModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.favorites.favoriteAppItems -import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource.NappletsFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.RootSiteEvent import com.vitorpamplona.quartz.nip5dNapplets.NamedNappletEvent import com.vitorpamplona.quartz.nip5dNapplets.RootNappletEvent +import com.vitorpamplona.quartz.utils.TimeUtils import org.jetbrains.compose.resources.StringResource -import com.vitorpamplona.amethyst.commons.R as CommonsR /** How many of the most recent history entries the idle browser home surfaces under "Recent". */ private const val RECENTS_LIMIT = 12 @@ -150,13 +151,14 @@ private fun BrowserLauncher( accountViewModel: AccountViewModel, nav: INav, ) { - val context = LocalContext.current + val appServices = LocalAppServices.current + val appLauncher = rememberAppLauncher() val appStillLoadingStr = stringRes(Res.string.favorite_app_still_loading) - val apps by Amethyst.instance.favoriteApps.favorites + val apps by appServices.favoriteApps.favorites .collectAsStateWithLifecycle() - val history by Amethyst.instance.browserHistory.history + val history by appServices.browserHistory.history .collectAsStateWithLifecycle() - val iconKeys by Amethyst.instance.browserIcons.keys + val iconKeys by appServices.browserIcons.keys .collectAsStateWithLifecycle() // Fetch favorited nsite/napplet manifests up front so tapping one launches immediately instead of @@ -227,7 +229,7 @@ private fun BrowserLauncher( fun open(text: String) { val target = OmniboxInput.resolve(text) ?: return - FavoriteAppLauncher.launchUrl(context, target.url, target.forceTor) + appLauncher.launchUrl(target.url, target.forceTor) } // Pin/unpin a plain web URL by its favorite id. Shared by the suggestion list and the Recent rows. @@ -236,10 +238,10 @@ private fun BrowserLauncher( label: String, ) { val id = "url:$url" - if (Amethyst.instance.favoriteApps.isFavorite(id)) { - Amethyst.instance.favoriteApps.remove(id) + if (appServices.favoriteApps.isFavorite(id)) { + appServices.favoriteApps.remove(id) } else { - Amethyst.instance.favoriteApps.add(FavoriteApp.WebApp(url, label.ifBlank { OmniboxInput.hostOf(url) ?: url }, System.currentTimeMillis())) + appServices.favoriteApps.add(FavoriteApp.WebApp(url, label.ifBlank { OmniboxInput.hostOf(url) ?: url }, TimeUtils.nowMillis())) } } @@ -291,7 +293,7 @@ private fun BrowserLauncher( historyUrls = historyUrls, onOpen = { open(it.url) }, onToggleFavorite = { toggleFavorite(it.url, it.label) }, - onRemoveFromHistory = { Amethyst.instance.browserHistory.remove(it) }, + onRemoveFromHistory = { appServices.browserHistory.remove(it) }, modifier = contentModifier, ) else -> { @@ -306,12 +308,12 @@ private fun BrowserLauncher( suggested = suggested, nsites = followedNsites, napplets = followedNapplets, - onOpenApp = { FavoriteAppLauncher.launch(context, it, appStillLoadingStr) }, - onRemoveApp = { Amethyst.instance.favoriteApps.remove(it.id) }, - onAddApp = { Amethyst.instance.favoriteApps.add(it) }, + onOpenApp = { appLauncher.launch(it, appStillLoadingStr) }, + onRemoveApp = { appServices.favoriteApps.remove(it.id) }, + onAddApp = { appServices.favoriteApps.add(it) }, onOpenUrl = { open(it) }, onToggleRecentFavorite = { entry -> toggleFavorite(entry.url, entry.title.ifBlank { entry.host }) }, - onRemoveRecent = { Amethyst.instance.browserHistory.remove(it) }, + onRemoveRecent = { appServices.browserHistory.remove(it) }, modifier = contentModifier, ) } @@ -348,7 +350,7 @@ private fun OmniBar( onValueChange = onValueChange, modifier = Modifier.weight(1f), singleLine = true, - placeholder = { Text(stringRes(CommonsR.string.browser_address_hint)) }, + placeholder = { Text(stringRes(Res.string.browser_address_hint)) }, keyboardOptions = KeyboardOptions( capitalization = KeyboardCapitalization.None, @@ -572,7 +574,8 @@ private fun SuggestedRow( onClick: () -> Unit, onAddFavorite: () -> Unit, ) { - val iconModel = remember(entry, iconKeys) { OmniboxInput.hostOf(entry.app.url)?.let(Amethyst.instance.browserIcons::iconModelFor) } + val appServices = LocalAppServices.current + val iconModel = remember(entry, iconKeys) { OmniboxInput.hostOf(entry.app.url)?.let(appServices.browserIcons::iconModelFor) } Row( modifier = Modifier @@ -644,7 +647,7 @@ private fun List.toDiscoverApps( /** Resolve a cached nsite/napplet manifest note into a launchable favorite + its description, or null. */ private fun Note.toDiscoverNostrApp(): DiscoverNostrApp? { val event = event ?: return null - val coordinate = FavoriteAppLauncher.coordinateOf(event) + val coordinate = favoriteCoordinateOf(event) val label: String val description: String? when (event) { @@ -803,7 +806,8 @@ private fun SiteIcon( iconKeys: Set, modifier: Modifier = Modifier, ) { - val model = remember(host, iconKeys) { Amethyst.instance.browserIcons.iconModelFor(host) } + val appServices = LocalAppServices.current + val model = remember(host, iconKeys) { appServices.browserIcons.iconModelFor(host) } val symbol = if (isFavorite) MaterialSymbols.Star else MaterialSymbols.Public val tint = MaterialTheme.colorScheme.onSurfaceVariant if (model == null) { @@ -831,7 +835,7 @@ private fun hostOf(url: String): String = OmniboxInput.hostOf(url) ?: url * host stays, because that is the part that differs. Falls back to the host when the URL has * nothing more to say. */ -internal fun recentSubtitle( +fun recentSubtitle( url: String, host: String, ): String { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentAttestationScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/AgentAttestationScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentAttestationScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/AgentAttestationScreen.kt index b235b71514..451c3a7095 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentAttestationScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/AgentAttestationScreen.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.buzz +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -91,6 +91,7 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_attest_remove import com.vitorpamplona.amethyst.commons.resources.buzz_attest_signed_title import com.vitorpamplona.amethyst.commons.resources.buzz_attest_topbar import com.vitorpamplona.amethyst.commons.resources.buzz_attest_warning +import com.vitorpamplona.amethyst.commons.search.calendar.SearchDate import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton @@ -104,16 +105,13 @@ import com.vitorpamplona.quartz.nip01Core.core.isValid import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonPrimitive -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import java.util.TimeZone // Common event kinds an agent might be restricted to — suggestions for the "Restrict to kind" field; // any 0–65535 is still accepted by free numeric entry. @@ -654,15 +652,18 @@ private fun parseOptionalUnix(input: String): OptionalUnix? { return OptionalUnix(parsed) } -private val UNIX_ECHO_FORMAT = - SimpleDateFormat("yyyy-MM-dd HH:mm 'UTC'", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") } - -/** A readable UTC rendering of an entered epoch-seconds string, or null when it's blank/out of range. */ +/** A readable UTC rendering (`yyyy-MM-dd HH:mm UTC`) of an entered epoch-seconds string, or null when it's blank/out of range. */ private fun unixEcho(input: String): String? { if (input.isBlank()) return null val secs = input.toLongOrNull()?.takeIf { it in 0..4294967295L } ?: return null - return UNIX_ECHO_FORMAT.format(Date(secs * 1000)) + val day = SearchDate.civilFromDays(secs / SECONDS_PER_DAY) + val secondOfDay = secs % SECONDS_PER_DAY + val hours = (secondOfDay / 3600).toString().padStart(2, '0') + val minutes = (secondOfDay % 3600 / 60).toString().padStart(2, '0') + return "${day.ymd()} $hours:$minutes UTC" } +private const val SECONDS_PER_DAY = 86_400L + /** Serializes the `auth` tag to a JSON array string (values are hex / canonical ASCII). */ private fun OwnerAttestation.toTagJson(): String = toTag().joinToString(prefix = "[", postfix = "]") { "\"$it\"" } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentConsoleScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/AgentConsoleScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentConsoleScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/AgentConsoleScreen.kt index cb4940267a..e2110fda0e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentConsoleScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/AgentConsoleScreen.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.buzz +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -55,15 +55,16 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.buzz.AgentFleetMetrics import com.vitorpamplona.amethyst.commons.model.buzz.AgentUsageSummary import com.vitorpamplona.amethyst.commons.model.buzz.TokenTotals import com.vitorpamplona.amethyst.commons.model.navigation.Route +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.AgentAttestationScreen import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel /** @@ -86,7 +87,7 @@ fun AgentConsoleScreen( nav: INav, ) { val pubkey = accountViewModel.account.userProfile().pubkeyHex - val viewModel: AgentConsoleViewModel = viewModel(key = "AgentConsole-$relayUrl-$pubkey") + val viewModel: AgentConsoleViewModel = rememberViewModel(key = "AgentConsole-$relayUrl-$pubkey") { AgentConsoleViewModel() } viewModel.bind(accountViewModel.account, relayUrl) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentConsoleViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/AgentConsoleViewModel.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentConsoleViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/AgentConsoleViewModel.kt index bf21ff22b3..2a3249f90f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentConsoleViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/AgentConsoleViewModel.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.buzz +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel @@ -41,7 +41,9 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.subscribeAsFlow import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.utils.concurrent.ConcurrentSet import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -50,7 +52,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import java.util.Collections +import kotlin.concurrent.Volatile /** * Backing ViewModel for the [AgentConsoleScreen] — the workspace owner's read-only @@ -94,7 +96,7 @@ class AgentConsoleViewModel : ViewModel() { private var watchJob: Job? = null /** Dedups ephemeral frames across relays and re-emissions (accessed off multiple readers). */ - private val observerSeen = Collections.synchronizedSet(HashSet()) + private val observerSeen = ConcurrentSet() /** Binds to [account] scoped to the community [relayUrl] — the console shows that community's fleet. */ fun bind( @@ -226,7 +228,7 @@ class AgentConsoleViewModel : ViewModel() { // Observer frames are ephemeral and unbounded over a long session; keep the // dedup set from growing without limit. We only render the newest ~200 rows, // so dropping the seen-history occasionally risks at most a stale duplicate. - if (observerSeen.size > MAX_OBSERVER_ROWS * 4) observerSeen.clear() + if (observerSeen.size() > MAX_OBSERVER_ROWS * 4) observerSeen.clear() val fresh = events .filterIsInstance() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.kt index b519809b21..73ece7e012 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.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.buzz +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat @@ -84,13 +84,13 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.buzz.AgentWorkItem import com.vitorpamplona.amethyst.commons.model.buzz.AgentWorkKind import com.vitorpamplona.amethyst.commons.model.buzz.AgentWorkState +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.note.LoadUser @@ -123,7 +123,7 @@ fun AgentWorkBoardScreen( ) { val me = remember(accountViewModel) { accountViewModel.account.userProfile().pubkeyHex } val canWrite = remember(accountViewModel) { accountViewModel.account.isWriteable() } - val viewModel: AgentWorkBoardViewModel = viewModel(key = "AgentWorkBoard-$relayUrl-$channelId") + val viewModel: AgentWorkBoardViewModel = rememberViewModel(key = "AgentWorkBoard-$relayUrl-$channelId") { AgentWorkBoardViewModel() } viewModel.bind(accountViewModel.account, channelId, relayUrl) DisposableEffect(channelId, relayUrl) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/AgentWorkBoardViewModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/AgentWorkBoardViewModel.kt index 99e32dec23..b0b105b490 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/AgentWorkBoardViewModel.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.buzz +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -46,6 +46,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.IO import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -59,6 +60,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlin.concurrent.Volatile /** * Backing ViewModel for the merged **Agent work** board — one channel, both agent protocols folded diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzCanvasScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/BuzzCanvasScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzCanvasScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/BuzzCanvasScreen.kt index ccf9e302cc..ce7eeeb538 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzCanvasScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/BuzzCanvasScreen.kt @@ -18,9 +18,8 @@ * 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.buzz +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz -import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -63,6 +62,7 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_canvas_empty import com.vitorpamplona.amethyst.commons.resources.buzz_canvas_save import com.vitorpamplona.amethyst.commons.resources.buzz_canvas_title import com.vitorpamplona.amethyst.commons.resources.cancel +import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton @@ -73,6 +73,7 @@ import com.vitorpamplona.quartz.buzz.workspace.isBuzzDm import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip29RelayGroups.GroupId import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -222,7 +223,7 @@ private fun CanvasEditor( val scope = rememberCoroutineScope() // Back cancels the edit and returns to the rendered canvas rather than leaving the screen. - BackHandler(enabled = !saving, onBack = onClose) + PlatformBackHandler(enabled = !saving, onBack = onClose) Scaffold( topBar = { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt index 5267f16fa3..08e52e8dc4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/BuzzDmListScreen.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.buzz +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -62,7 +62,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.User @@ -84,6 +83,7 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_dm_new import com.vitorpamplona.amethyst.commons.resources.buzz_dm_title import com.vitorpamplona.amethyst.commons.resources.cancel import com.vitorpamplona.amethyst.commons.resources.remove_from_messages +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.note.TimeAgoLabels @@ -119,7 +119,7 @@ fun BuzzDmListScreen( nav: INav, ) { val pubkey = accountViewModel.account.userProfile().pubkeyHex - val viewModel: BuzzDmListViewModel = viewModel(key = "BuzzDmList-$relayUrl-$pubkey") + val viewModel: BuzzDmListViewModel = rememberViewModel(key = "BuzzDmList-$relayUrl-$pubkey") { BuzzDmListViewModel() } viewModel.bind(accountViewModel.account, relayUrl) val rows by viewModel.rows.collectAsStateWithLifecycle() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/BuzzDmListViewModel.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/BuzzDmListViewModel.kt index 1a0c63157f..f76b6b9ab6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/BuzzDmListViewModel.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.buzz +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel @@ -46,7 +46,9 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip29RelayGroups.GroupId import com.vitorpamplona.quartz.nipC7Chats.ChatEvent +import com.vitorpamplona.quartz.utils.concurrent.ConcurrentMap import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -55,7 +57,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import java.util.concurrent.ConcurrentHashMap +import kotlin.concurrent.Volatile /** * Backing ViewModel for [BuzzDmListScreen] — the user's Buzz direct-message inbox. @@ -87,7 +89,7 @@ class BuzzDmListViewModel : ViewModel() { private var liveJob: Job? = null /** channelId -> relay it was discovered on (from the 44100 provenance). */ - private val memberChannels = ConcurrentHashMap() + private val memberChannels = ConcurrentMap() private val _rows = MutableStateFlow>(emptyList()) val rows: StateFlow> = _rows.asStateFlow() @@ -217,7 +219,9 @@ class BuzzDmListViewModel : ViewModel() { /** Fetch the NIP-29 directory (39000-39003) of every discovered channel so its `t`/roster load. */ private suspend fun fetchMetadata(account: Account) { val byRelay = - memberChannels.entries + memberChannels + .asMap() + .entries .groupBy({ it.value }, { it.key }) .mapValues { (_, ids) -> listOf(Filter(kinds = RELAY_GROUP_METADATA_KINDS, tags = mapOf("d" to ids))) } if (byRelay.isEmpty()) return @@ -233,7 +237,9 @@ class BuzzDmListViewModel : ViewModel() { val myPubkey = account.userProfile().pubkeyHex val hidden = BuzzDmRegistry.hiddenFor(myPubkey) val (hiddenDms, visibleDms) = - memberChannels.entries + memberChannels + .asMap() + .entries .mapNotNull { (channelId, relay) -> val channel = LocalCache.getOrCreateRelayGroupChannel(GroupId(channelId, relay)) val metadata = channel.event ?: return@mapNotNull null @@ -323,7 +329,13 @@ class BuzzDmListViewModel : ViewModel() { .filterValues { it in scoped } var changed = false memberships.forEach { (channelId, relay) -> - if (memberChannels.put(channelId, relay) == null) changed = true + // One atomic insert decides "new", so a concurrent seed of the same + // channel can't make this pass skip its row rebuild. + if (memberChannels.putIfAbsent(channelId, relay) == null) { + changed = true + } else { + memberChannels[channelId] = relay + } } // A kind-44101 takes the membership away: drop the row rather than leaving a // conversation the relay no longer lets us read. @@ -338,7 +350,7 @@ class BuzzDmListViewModel : ViewModel() { .let { BuzzChannelInvites.latestPerChannel(it) } .filterValues { it.removed } .keys - val gone = memberChannels.keys.filter { it in withdrawn } + val gone = memberChannels.asMap().keys.filter { it in withdrawn } if (gone.isNotEmpty()) { gone.forEach { memberChannels.remove(it) } changed = true diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzInviteScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/BuzzInviteScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzInviteScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/BuzzInviteScreen.kt index 2d77460d81..7b4d6677fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzInviteScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/BuzzInviteScreen.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.buzz +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -47,7 +47,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -69,9 +68,10 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_invite_title import com.vitorpamplona.amethyst.commons.resources.buzz_invite_workspace import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.platform.AppLauncher +import com.vitorpamplona.amethyst.commons.ui.platform.rememberAppLauncher import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher import com.vitorpamplona.quartz.buzz.invite.BuzzInviteLink import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.utils.TimeUtils @@ -85,7 +85,7 @@ import kotlinx.coroutines.launch * a flow the Buzz web app already implements and drives through `window.nostr`. So rather than * re-implement the legally-sensitive age/privacy consent natively, this confirms the workspace, * marks its relay as a Buzz dialect, and hands the URL to the in-app `window.nostr` browser - * ([FavoriteAppLauncher.launchUrl] → the sandboxed WebView), where the SPA signs the claim with + * ([AppLauncher.launchUrl] → the sandboxed WebView), where the SPA signs the claim with * the user's key. The user returns here (or to the app) once enrolled. */ @Composable @@ -95,7 +95,7 @@ fun BuzzInviteScreen( nav: INav, ) { val invite = remember(link) { BuzzInviteLink.parse(link) } - val context = LocalContext.current + val appLauncher = rememberAppLauncher() val scope = rememberCoroutineScope() var launched by remember { mutableStateOf(false) } @@ -201,7 +201,7 @@ fun BuzzInviteScreen( accountViewModel.account.buzzWorkspaces.join(relay) scope.launch { accountViewModel.account.relayAuthLedger.setDecision(relay.url, RelayAuthDecision.ALLOW) } } - FavoriteAppLauncher.launchUrl(context, link) + appLauncher.launchUrl(link) launched = true }, enabled = !expired, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzJoinReconnect.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/BuzzJoinReconnect.kt similarity index 81% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzJoinReconnect.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/BuzzJoinReconnect.kt index 9d419d535b..a2cea31f96 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzJoinReconnect.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/BuzzJoinReconnect.kt @@ -18,10 +18,10 @@ * 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.buzz +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz -import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.service.resourceusage.UsageKeys +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.service.resourceusage.UsageKeys import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient /** @@ -44,10 +44,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient * reconnect — it hands off to the in-app browser rather than reading a `#p=me`-gated * subscription — so `relay.trigger.buzz` undercounts joins, not re-challenges. */ -internal fun reconnectPoolAfterJoin(client: INostrClient) { - // Guarded like every other ledger write that reaches the application singleton - // (MediaPlayTimeTracker, the workers): a diagnostics counter must never break a - // user-visible join, and `Amethyst.instance` is lateinit. - runCatching { Amethyst.instance.resourceUsage.add(UsageKeys.relayTrigger(UsageKeys.TRIGGER_BUZZ), 1) } +fun reconnectPoolAfterJoin(client: INostrClient) { + LocalCache.appHost.usageCounter.add(UsageKeys.relayTrigger(UsageKeys.TRIGGER_BUZZ), 1) client.reconnect(onlyIfChanged = false, ignoreRetryDelays = true) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/JobBoardScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/JobBoardScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/JobBoardScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/JobBoardScreen.kt index e7a6992928..096ae77ae1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/JobBoardScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/JobBoardScreen.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.buzz +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.RepeatMode @@ -78,7 +78,6 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -86,6 +85,7 @@ import com.vitorpamplona.amethyst.commons.model.buzz.JobState import com.vitorpamplona.amethyst.commons.model.buzz.JobView import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.buzz_job_board_title +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.note.LoadUser @@ -120,7 +120,7 @@ fun JobBoardScreen( nav: INav, ) { val me = accountViewModel.account.userProfile().pubkeyHex - val viewModel: JobBoardViewModel = viewModel(key = "JobBoard-$relayUrl-$channelId") + val viewModel: JobBoardViewModel = rememberViewModel(key = "JobBoard-$relayUrl-$channelId") { JobBoardViewModel() } viewModel.bind(accountViewModel.account, channelId, relayUrl) DisposableEffect(channelId, relayUrl) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/JobBoardViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/JobBoardViewModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/JobBoardViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/JobBoardViewModel.kt index c2fd829cb3..c27663ed3c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/JobBoardViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/JobBoardViewModel.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.buzz +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -31,6 +31,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -38,6 +39,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch +import kotlin.concurrent.Volatile /** * Backing ViewModel for the [JobBoardScreen] — the shared backlog of one Buzz channel. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/RelayStatusBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/RelayStatusBar.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/RelayStatusBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/RelayStatusBar.kt index 53371635cb..9caac24011 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/RelayStatusBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/RelayStatusBar.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.buzz +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.RepeatMode @@ -55,7 +55,6 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -88,7 +87,7 @@ fun RelayStatusBar( .connectedRelaysFlow() .collectAsStateWithLifecycle() val authState = - Amethyst.instance.authCoordinator.receiver.authStateFlow + accountViewModel.host.relayAuthState .collectAsStateWithLifecycle() val buzzState = BuzzRelayDialect.flow.collectAsStateWithLifecycle() val info by loadRelayInfo(relay) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/WorkflowRunBoardScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/WorkflowRunBoardScreen.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/WorkflowRunBoardScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/WorkflowRunBoardScreen.kt index 9084414321..8fc2325e25 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/WorkflowRunBoardScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/WorkflowRunBoardScreen.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.buzz +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat @@ -92,7 +92,6 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -158,6 +157,7 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_workflow_triggered_toas import com.vitorpamplona.amethyst.commons.resources.buzz_workflow_view_pr import com.vitorpamplona.amethyst.commons.resources.buzz_workflow_waiting_for_approval import com.vitorpamplona.amethyst.commons.resources.buzz_workflow_waiting_on +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.note.LoadUser @@ -194,7 +194,7 @@ fun WorkflowRunBoardScreen( ) { val me = remember(accountViewModel) { accountViewModel.account.userProfile().pubkeyHex } val canWrite = remember(accountViewModel) { accountViewModel.account.isWriteable() } - val viewModel: WorkflowRunBoardViewModel = viewModel(key = "WorkflowRunBoard-$relayUrl-$channelId") + val viewModel: WorkflowRunBoardViewModel = rememberViewModel(key = "WorkflowRunBoard-$relayUrl-$channelId") { WorkflowRunBoardViewModel() } viewModel.bind(accountViewModel.account, channelId, relayUrl) DisposableEffect(channelId, relayUrl) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/WorkflowRunBoardViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/WorkflowRunBoardViewModel.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/WorkflowRunBoardViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/WorkflowRunBoardViewModel.kt index 1839c82f2c..4ac01887d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/WorkflowRunBoardViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/buzz/WorkflowRunBoardViewModel.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.buzz +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel @@ -45,6 +45,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.IO import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -58,6 +59,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlin.concurrent.Volatile /** One selectable workflow definition (kind-30620) — its UUID `d` tag, optional name, and YAML recipe. */ @Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupInfoScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/cordnGroup/CordnGroupInfoScreen.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupInfoScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/cordnGroup/CordnGroupInfoScreen.kt index fae0dec2d3..6e7b39807f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupInfoScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/cordnGroup/CordnGroupInfoScreen.kt @@ -18,9 +18,8 @@ * 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.chats.cordnGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup -import android.util.Log import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -132,17 +131,18 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowU import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.observeUserNameByHex +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SectionCollapse +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SectionExpand import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.CoordinatorIdentityRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SectionCollapse -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SectionExpand import com.vitorpamplona.quartz.cordn.spec00Coordinator.JoinRequest import com.vitorpamplona.quartz.cordn.spec01GroupMetadata.CordnGroupMetadata import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt index 5a39b183fd..f7a3ea083a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt @@ -18,9 +18,8 @@ * 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.chats.publicChannels.concord +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord -import android.content.Intent import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -57,7 +56,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboard -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow @@ -106,6 +104,7 @@ import com.vitorpamplona.amethyst.commons.resources.copy_to_clipboard import com.vitorpamplona.amethyst.commons.resources.leave import com.vitorpamplona.amethyst.commons.resources.more_options import com.vitorpamplona.amethyst.commons.resources.quick_action_share +import com.vitorpamplona.amethyst.commons.ui.components.rememberTextSharer import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar @@ -115,9 +114,9 @@ import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordDirectInviteDialog import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelPreviewLoader import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChannelId import com.vitorpamplona.quartz.concord.cord04Roles.ConcordPermissions import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -632,7 +631,7 @@ private fun rememberConcordDisplayName( * so leaving is what actually retires the community for them. */ @Composable -internal fun ConcordLeaveDialog( +fun ConcordLeaveDialog( communityName: String, isOwner: Boolean, onDismiss: () -> Unit, @@ -777,7 +776,7 @@ private fun InviteLinkDialog( val concordInviteTitleStr = stringRes(Res.string.concord_invite_title) val clipboard = LocalClipboard.current val scope = rememberCoroutineScope() - val context = LocalContext.current + val textSharer = rememberTextSharer() AlertDialog( onDismissRequest = onDismiss, title = { Text(stringRes(Res.string.concord_invite_title)) }, @@ -796,13 +795,7 @@ private fun InviteLinkDialog( }, confirmButton = { TextButton(onClick = { - val send = - Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, link) - } - context.startActivity(Intent.createChooser(send, concordInviteTitleStr)) + textSharer.share(link, null, concordInviteTitleStr) onDismiss() }) { Text(stringRes(Res.string.quick_action_share)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordCreateScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordCreateScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordCreateScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordCreateScreen.kt index 6efc450133..3355c41e4d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordCreateScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordCreateScreen.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.chats.publicChannels.concord +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordEditScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordEditScreen.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordEditScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordEditScreen.kt index 87d4f4a264..ca2e82a4fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordEditScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordEditScreen.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.chats.publicChannels.concord +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt index d7ba2251db..eb4963438d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.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.chats.publicChannels.concord +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -77,6 +77,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar import com.vitorpamplona.amethyst.commons.ui.note.timeAgo import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.platform.rememberConcordImageModel import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.ConcordPendingDirectInvites diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordImageUploader.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordImageUploader.kt similarity index 83% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordImageUploader.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordImageUploader.kt index 9c3c5470ac..bef5c6f91b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordImageUploader.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordImageUploader.kt @@ -18,22 +18,24 @@ * 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.chats.publicChannels.concord +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord -import android.content.Context -import android.net.Uri import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.ui.loadStringRes -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.quartz.concord.cord02Community.ImagePointer import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.utils.ciphers.AESGCM import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.withContext /** @@ -52,16 +54,14 @@ class ConcordImageUploader( private val account: Account, ) { /** - * Compresses, strips, AES-256-GCM-encrypts and uploads the picked [uri], returning its pointer. + * Compresses, strips, AES-256-GCM-encrypts and uploads the picked [media], returning its pointer. * - * Runs on [Dispatchers.IO]: the compression/upload pipeline asserts it is off the main thread - * ([MediaCompressor] calls `checkNotInMainThread`), and callers launch this from a Compose - * `rememberCoroutineScope()`, which is Main-dispatched — so without this switch the first step - * throws before any bytes leave the device. + * Runs on [Dispatchers.IO]: compression, encryption and the upload all block, and callers launch + * this from a Compose `rememberCoroutineScope()`, which is Main-dispatched. */ suspend fun uploadEncrypted( - uri: Uri, - context: Context, + media: SelectedMedia, + uploader: MediaUploader, ): ImagePointer = withContext(Dispatchers.IO) { // Fresh random key + nonce per image; we hold onto them to build the pointer below since the @@ -70,15 +70,15 @@ class ConcordImageUploader( val finalState = UploadOrchestrator().uploadEncrypted( - uri = uri, - mimeType = context.contentResolver.getType(uri), + uri = media.uri, + mimeType = media.mimeType, alt = null, contentWarningReason = null, compressionQuality = CompressorQuality.MEDIUM, encrypt = cipher, server = resolveBlossomServer(), account = account, - context = context, + uploader = uploader, ) val result = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordInviteLinksScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordInviteLinksScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordInviteLinksScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordInviteLinksScreen.kt index 76190050de..13d08f56d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordInviteLinksScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordInviteLinksScreen.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.chats.publicChannels.concord +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -79,13 +79,13 @@ import com.vitorpamplona.amethyst.commons.resources.copy_to_clipboard import com.vitorpamplona.amethyst.commons.resources.more_options import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.DateTimeStyle +import com.vitorpamplona.amethyst.commons.ui.note.formatDateTime import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.concord.cord05Invites.ConcordInviteListEntry import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch -import java.text.DateFormat -import java.util.Date import com.vitorpamplona.amethyst.commons.icons.symbols.Icon as SymbolIcon /** What the screen is currently showing. The unreadable case is deliberately not "empty" — see below. */ @@ -287,7 +287,7 @@ private fun InviteLinkRow( // a long prefix, so they are useless as labels until well past where the row wraps. Text(link.token.take(8), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.bodyLarge) Text( - stringRes(Res.string.concord_invite_links_created, DateFormat.getDateInstance(DateFormat.MEDIUM).format(Date(link.createdAt * 1000))), + stringRes(Res.string.concord_invite_links_created, formatDateTime(link.createdAt * 1000, DateTimeStyle.MEDIUM, DateTimeStyle.NONE)), style = MaterialTheme.typography.bodySmall, ) Text(link.url, style = MaterialTheme.typography.bodySmall, maxLines = 1, overflow = TextOverflow.Ellipsis) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt index d715b3089e..5a69e72520 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt @@ -18,12 +18,8 @@ * 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.chats.publicChannels.concord +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.PickVisualMediaRequest -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -51,13 +47,13 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalClipboard -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -75,16 +71,19 @@ import com.vitorpamplona.amethyst.commons.resources.concord_create_title import com.vitorpamplona.amethyst.commons.resources.concord_edit_banner_hint import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details import com.vitorpamplona.amethyst.commons.resources.remove +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage +import com.vitorpamplona.amethyst.commons.ui.components.rememberLongNotice import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.RenderRelayIcon +import com.vitorpamplona.amethyst.commons.ui.platform.rememberConcordImageModel import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.MediumRelayIconModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.quartz.concord.cord02Community.ImagePointer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl @@ -156,29 +155,31 @@ private fun ConcordIconHero( accountViewModel: AccountViewModel, ) { val autoPlayGif by accountViewModel.settings.autoPlayVideosFlow.collectAsStateWithLifecycle() - val context = LocalContext.current + val notice = rememberLongNotice() val scope = rememberCoroutineScope() var uploading by remember { mutableStateOf(false) } val iconModel = rememberConcordImageModel(icon.value, accountViewModel) - val picker = - rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> - if (uri == null) return@rememberLauncherForActivityResult + var picking by rememberSaveable { mutableStateOf(false) } + if (picking) { + GallerySelectSingle(imagesOnly = true) { media -> + picking = false + if (media == null) return@GallerySelectSingle uploading = true scope.launch { try { - icon.value = ConcordImageUploader(accountViewModel.account).uploadEncrypted(uri, context) + icon.value = ConcordImageUploader(accountViewModel.account).uploadEncrypted(media, accountViewModel.host.mediaUploader) } catch (e: CancellationException) { throw e } catch (e: Exception) { Log.w("ConcordImageUpload", "Community icon upload failed", e) - val msg = e.message?.takeIf { it.isNotBlank() } ?: loadStringRes(Res.string.failed_to_upload_media_no_details) - Toast.makeText(context, msg, Toast.LENGTH_LONG).show() + notice.show(e.message?.takeIf { it.isNotBlank() } ?: loadStringRes(Res.string.failed_to_upload_media_no_details)) } finally { uploading = false } } } + } Column(horizontalAlignment = Alignment.CenterHorizontally) { Box( @@ -186,7 +187,7 @@ private fun ConcordIconHero( Modifier .size(104.dp) .clip(CircleShape) - .clickable(enabled = !uploading) { picker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }, + .clickable(enabled = !uploading) { picking = true }, contentAlignment = Alignment.Center, ) { RobohashFallbackAsyncImage( @@ -210,7 +211,7 @@ private fun ConcordIconHero( Modifier .padding(top = 8.dp) .clip(CircleShape) - .clickable(enabled = !uploading) { picker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) } + .clickable(enabled = !uploading) { picking = true } .padding(horizontal = 8.dp, vertical = 4.dp), ) } @@ -227,29 +228,31 @@ private fun ConcordBannerHero( banner: MutableState, accountViewModel: AccountViewModel, ) { - val context = LocalContext.current + val notice = rememberLongNotice() val scope = rememberCoroutineScope() var uploading by remember { mutableStateOf(false) } val bannerModel = rememberConcordImageModel(banner.value, accountViewModel) - val picker = - rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> - if (uri == null) return@rememberLauncherForActivityResult + var picking by rememberSaveable { mutableStateOf(false) } + if (picking) { + GallerySelectSingle(imagesOnly = true) { media -> + picking = false + if (media == null) return@GallerySelectSingle uploading = true scope.launch { try { - banner.value = ConcordImageUploader(accountViewModel.account).uploadEncrypted(uri, context) + banner.value = ConcordImageUploader(accountViewModel.account).uploadEncrypted(media, accountViewModel.host.mediaUploader) } catch (e: CancellationException) { throw e } catch (e: Exception) { Log.w("ConcordImageUpload", "Community banner upload failed", e) - val msg = e.message?.takeIf { it.isNotBlank() } ?: loadStringRes(Res.string.failed_to_upload_media_no_details) - Toast.makeText(context, msg, Toast.LENGTH_LONG).show() + notice.show(e.message?.takeIf { it.isNotBlank() } ?: loadStringRes(Res.string.failed_to_upload_media_no_details)) } finally { uploading = false } } } + } Box( modifier = @@ -258,7 +261,7 @@ private fun ConcordBannerHero( .aspectRatio(3f) .clip(RoundedCornerShape(12.dp)) .background(MaterialTheme.colorScheme.surfaceVariant) - .clickable(enabled = !uploading) { picker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }, + .clickable(enabled = !uploading) { picking = true }, contentAlignment = Alignment.Center, ) { if (bannerModel != null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupBrowseScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupBrowseScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupBrowseScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupBrowseScreen.kt index 0f7899954d..aa0dbde732 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupBrowseScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupBrowseScreen.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.chats.publicChannels.relayGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -59,12 +59,12 @@ import com.vitorpamplona.amethyst.commons.resources.relay_group_browse_title import com.vitorpamplona.amethyst.commons.resources.relay_group_browse_your_relays import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelaySuggestionState +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.ShowRelaySuggestionList import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.HalfHorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.PopupUpEffect import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelaySuggestionState -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.ShowRelaySuggestionList import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupServerList.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupServerList.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupServerList.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupServerList.kt index f8d30ed04d..9da21e6e66 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupServerList.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupServerList.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.chats.publicChannels.relayGroup +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/CommunitiesScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/communities/list/CommunitiesScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/CommunitiesScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/communities/list/CommunitiesScreen.kt index 56636c4501..c0ce4a941c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/CommunitiesScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/communities/list/CommunitiesScreen.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.communities.list +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.list import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Row @@ -49,11 +49,11 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.list.datasource.CommunitiesListFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.ChannelCardCompose import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.list.datasource.CommunitiesListFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.ChannelCardCompose import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/CommunitiesTopBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/communities/list/CommunitiesTopBar.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/CommunitiesTopBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/communities/list/CommunitiesTopBar.kt index 3dfeaa1535..37c093311a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/CommunitiesTopBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/communities/list/CommunitiesTopBar.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.communities.list +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.list import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/datasource/CommunitiesListFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/communities/list/datasource/CommunitiesListFilterAssemblerSubscription.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/datasource/CommunitiesListFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/communities/list/datasource/CommunitiesListFilterAssemblerSubscription.kt index ecd20288d1..d60054fc82 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/datasource/CommunitiesListFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/communities/list/datasource/CommunitiesListFilterAssemblerSubscription.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.communities.list.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.list.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/CommunityRulesEditor.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/communities/newCommunity/CommunityRulesEditor.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/CommunityRulesEditor.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/communities/newCommunity/CommunityRulesEditor.kt index cb020b11d7..c816965d9b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/CommunityRulesEditor.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/communities/newCommunity/CommunityRulesEditor.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.communities.newCommunity +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.newCommunity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/communities/newCommunity/NewCommunityModel.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/communities/newCommunity/NewCommunityModel.kt index 8fe84605f6..b45bcf09dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/communities/newCommunity/NewCommunityModel.kt @@ -18,9 +18,8 @@ * 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.communities.newCommunity +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.newCommunity -import android.content.Context import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue @@ -39,11 +38,13 @@ import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_de import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.ui.loadStringRes -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions @@ -55,6 +56,7 @@ import com.vitorpamplona.quartz.nip72ModCommunities.rules.tags.PubkeyRuleTag import com.vitorpamplona.quartz.nip72ModCommunities.rules.tags.WotTag import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.launch import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @@ -271,12 +273,12 @@ class NewCommunityModel : ViewModel() { @OptIn(ExperimentalUuidApi::class) fun publish( - context: Context, + uploader: MediaUploader, onSuccess: () -> Unit, onError: (String, String) -> Unit, ) { try { - publishUnsafe(context, onSuccess, onError) + publishUnsafe(uploader, onSuccess, onError) } catch (e: SignerExceptions.ReadOnlyException) { viewModelScope.launch { onError( @@ -289,7 +291,7 @@ class NewCommunityModel : ViewModel() { @OptIn(ExperimentalUuidApi::class) private fun publishUnsafe( - context: Context, + uploader: MediaUploader, onSuccess: () -> Unit, onError: (String, String) -> Unit, ) { @@ -300,7 +302,7 @@ class NewCommunityModel : ViewModel() { try { val uploadedUrl = if (multiOrchestrator != null) { - uploadImageIfAny(context, myAccount, onError) ?: return@launch + uploadImageIfAny(uploader, myAccount, onError) ?: return@launch } else { null } @@ -371,7 +373,7 @@ class NewCommunityModel : ViewModel() { } private suspend fun uploadImageIfAny( - context: Context, + uploader: MediaUploader, myAccount: Account, onError: (String, String) -> Unit, ): String? { @@ -382,10 +384,10 @@ class NewCommunityModel : ViewModel() { orch.upload( alt = name.trim().ifBlank { "Community cover" }, contentWarningReason = null, - mediaQuality = MediaCompressor.intToCompressorQuality(mediaQualitySlider), + mediaQuality = CompressorQuality.fromSlider(mediaQualitySlider), server = serverToUse, account = myAccount, - context = context, + uploader = uploader, useH265 = false, stripMetadata = stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt index 105360e64f..7ae8c550e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.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.communities.newCommunity +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.newCommunity import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -58,13 +58,11 @@ import androidx.compose.runtime.remember 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.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -93,7 +91,10 @@ import com.vitorpamplona.amethyst.commons.resources.new_community_rules_section import com.vitorpamplona.amethyst.commons.resources.remove import com.vitorpamplona.amethyst.commons.resources.save import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.ShowImageUploadGallery import com.vitorpamplona.amethyst.commons.ui.components.Nip05OrPubkeyLine +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ActionTopBar @@ -102,14 +103,12 @@ import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoClickableRow +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelect -import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoClickableRow -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.RelayTag @@ -136,8 +135,7 @@ private fun CommunityFormScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val model: NewCommunityModel = viewModel() - val context = LocalContext.current + val model: NewCommunityModel = rememberViewModel { NewCommunityModel() } LaunchedEffect(accountViewModel.account) { model.init(accountViewModel.account) @@ -185,7 +183,7 @@ private fun CommunityFormScreen( }, onPost = { model.publish( - context = context, + uploader = accountViewModel.host.mediaUploader, onSuccess = { nav.popBack() }, onError = accountViewModel.toastManager::toast, ) @@ -201,7 +199,7 @@ private fun CommunityFormScreen( }, onPost = { model.publish( - context = context, + uploader = accountViewModel.host.mediaUploader, onSuccess = { nav.popBack() }, onError = accountViewModel.toastManager::toast, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/ChannelCardCompose.kt similarity index 91% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/ChannelCardCompose.kt index 0c396b5399..53707b4c73 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/ChannelCardCompose.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.discover +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable @@ -33,16 +33,16 @@ import com.vitorpamplona.amethyst.commons.ui.note.ClickableNote import com.vitorpamplona.amethyst.commons.ui.note.LongPressToQuickAction import com.vitorpamplona.amethyst.commons.ui.note.WatchNoteEvent import com.vitorpamplona.amethyst.commons.ui.note.calculateBackgroundColor +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip23LongForm.RenderLongFormThumb +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip28Chats.RenderPublicChatChannelThumb import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip51FollowSets.RenderFollowSetThumb +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip53LiveActivities.RenderLiveActivityThumb +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip72Communities.RenderCommunitiesThumb +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip90DVMs.RenderContentDVMThumb +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip99Classifieds.RenderClassifiedsThumb import com.vitorpamplona.amethyst.commons.ui.theme.HalfPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.RenderLongFormThumb -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.RenderPublicChatChannelThumb -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.RenderLiveActivityThumb -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.RenderCommunitiesThumb -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.RenderContentDVMThumb -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.RenderClassifiedsThumb import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.kt index 5f8625bc94..4b3d92beef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.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.discover.nip23LongForm +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip23LongForm import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt index c4314574a4..5f2aff74c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.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.discover.nip28Chats +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip28Chats import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -59,6 +59,8 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.DisplayAuthorBanner import com.vitorpamplona.amethyst.commons.ui.note.Gallery import com.vitorpamplona.amethyst.commons.ui.note.LoadPublicChatChannel +import com.vitorpamplona.amethyst.commons.ui.note.platform.LikeReaction +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZapReaction import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.HalfTopPadding import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder @@ -67,12 +69,11 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.util.equalImmutableLists import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.LikeReaction -import com.vitorpamplona.amethyst.ui.note.ZapReaction import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.launch @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt index 24ac29e322..ae2db40ec4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.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.discover.nip53LiveActivities +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip53LiveActivities import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -78,6 +78,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.launch @Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt index bfc457075a..8794710ba9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.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.discover.nip72Communities +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip72Communities import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -60,6 +60,8 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.LeftPictureLayout import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.DisplayAuthorBanner import com.vitorpamplona.amethyst.commons.ui.note.Gallery +import com.vitorpamplona.amethyst.commons.ui.note.platform.LikeReaction +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZapReaction import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.HalfTopPadding import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder @@ -68,14 +70,13 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.util.equalImmutableLists import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.LikeReaction -import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.launch @Immutable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt index 14ff0c3b40..d51348cd31 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.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.discover.nip90DVMs +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip90DVMs import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box @@ -46,6 +46,10 @@ import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.commons.ui.layouts.LeftPictureLayout import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.elements.BannerImage +import com.vitorpamplona.amethyst.commons.ui.note.platform.LikeReaction +import com.vitorpamplona.amethyst.commons.ui.note.platform.ZapReaction +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.dvms.FavoriteAlgoFeedToggle +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.dvms.observeAppDefinition import com.vitorpamplona.amethyst.commons.ui.theme.HalfTopPadding import com.vitorpamplona.amethyst.commons.ui.theme.RowColSpacing5dp import com.vitorpamplona.amethyst.commons.ui.theme.SimpleImageBorder @@ -54,10 +58,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.bitcoinColor import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.ui.theme.nip05 import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.LikeReaction -import com.vitorpamplona.amethyst.ui.note.ZapReaction -import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.FavoriteAlgoFeedToggle -import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.observeAppDefinition @Immutable data class DVMCard( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt index cd518375cc..3ae43f1201 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.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.discover.nip99Classifieds +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip99Classifieds import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nip99Classifieds.tags.PriceTag +import com.vitorpamplona.quartz.utils.parseBigDecimalOrNull @Immutable data class ClassifiedsThumb( @@ -141,7 +142,7 @@ fun InnerRenderClassifiedsThumb( card.price?.let { val priceTag = remember(card) { - val newAmount = it.amount.toBigDecimalOrNull()?.let { showAmountInteger(it) } ?: it.amount + val newAmount = parseBigDecimalOrNull(it.amount)?.let { showAmountInteger(it) } ?: it.amount if (it.frequency != null && it.currency != null) { "$newAmount ${it.currency}/${it.frequency}" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt index c33d41a4f6..403708c1cc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.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.dvms +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.dvms import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -41,14 +41,12 @@ import androidx.compose.ui.Alignment.Companion.BottomStart import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.payments.PaymentSource @@ -69,6 +67,8 @@ import com.vitorpamplona.amethyst.commons.service.nwc.nwcFailureDetail import com.vitorpamplona.amethyst.commons.service.nwc.nwcTimeoutMessage import com.vitorpamplona.amethyst.commons.ui.components.LoadNote import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage +import com.vitorpamplona.amethyst.commons.ui.components.ZapButtonConfig +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold @@ -76,26 +76,26 @@ import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.WatchNoteEvent import com.vitorpamplona.amethyst.commons.ui.note.elements.BannerImage +import com.vitorpamplona.amethyst.commons.ui.note.platform.ReusableZapButton import com.vitorpamplona.amethyst.commons.ui.screen.RenderFeedState import com.vitorpamplona.amethyst.commons.ui.screen.SaveableFeedState +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip90DVMs.DVMCard import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.dvms.DvmOfflineBanner +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.dvms.dal.DvmContentDiscoveryFeedViewModel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.dvms.rememberDvmHeartbeatFresh import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.SimpleImage75Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp +import com.vitorpamplona.amethyst.commons.ui.wallet.rememberWalletAppLauncher import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.components.ReusableZapButton -import com.vitorpamplona.amethyst.ui.components.ZapButtonConfig -import com.vitorpamplona.amethyst.ui.note.payViaIntent -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.DVMCard -import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.dal.DvmContentDiscoveryFeedViewModel import com.vitorpamplona.quartz.lightning.LnInvoiceUtil import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppMetadata import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.DvmContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip90Dvms.status.DvmStatusEvent +import com.vitorpamplona.quartz.utils.toLongValue import kotlinx.coroutines.launch @Composable @@ -266,7 +266,7 @@ fun PrepareViewContentDiscoveryModels( nav: INav, ) { val resultFeedViewModel: DvmContentDiscoveryFeedViewModel = - viewModel( + rememberViewModel( key = "NostrNIP90ContentDiscoveryFeedViewModel${dvm.pubkeyHex}$dvmRequestId", factory = DvmContentDiscoveryFeedViewModel.Factory(accountViewModel.account, dvmKey = dvm.pubkeyHex, requestId = dvmRequestId), ) @@ -398,7 +398,7 @@ fun DvmPaymentActions( val nwcPaymentRequest = stringRes(id = Res.string.nwc_payment_request) if (invoice != null) { - val context = LocalContext.current + val walletApp = rememberWalletAppLauncher() Button(onClick = { when (val source = accountViewModel.account.settings.defaultPaymentSource()) { is PaymentSource.ClinkDebit -> { @@ -436,9 +436,8 @@ fun DvmPaymentActions( } null -> { - payViaIntent( + walletApp.payInvoice( invoice, - context, noWalletFoundStr, onPaid = { onStatusUpdate(thankYou) @@ -452,7 +451,7 @@ fun DvmPaymentActions( }) { val amountInInvoice = try { - LnInvoiceUtil.getAmountInSats(invoice).toLong() + LnInvoiceUtil.getAmountInSats(invoice).toLongValue() } catch (_: Exception) { null } @@ -488,13 +487,15 @@ fun ZapDVMButton( nav: INav, ) { val config = - ZapButtonConfig( - grayTint = grayTint, - iconSize = iconSize, - showUserFinderSubscription = true, - zapAmountChoices = listOf(amount / 1000), - buttonText = "Zap ${(amount / 1000)} sats to the DVM", - ) + remember(grayTint, iconSize, amount) { + ZapButtonConfig( + grayTint = grayTint, + iconSize = iconSize, + showUserFinderSubscription = true, + zapAmountChoices = listOf(amount / 1000), + buttonText = "Zap ${(amount / 1000)} sats to the DVM", + ) + } ReusableZapButton( baseNote = baseNote, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmTopBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/DvmTopBar.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmTopBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/DvmTopBar.kt index 3dc424bd2c..d7640bd075 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmTopBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/DvmTopBar.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.dvms +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.dvms import androidx.compose.foundation.layout.Spacer import androidx.compose.material3.IconButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/FavoriteAlgoFeedToggle.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/FavoriteAlgoFeedToggle.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/FavoriteAlgoFeedToggle.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/FavoriteAlgoFeedToggle.kt index 226f2b0240..4f89453c1d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/FavoriteAlgoFeedToggle.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/FavoriteAlgoFeedToggle.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.dvms +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.dvms import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryFeedViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryFeedViewModel.kt similarity index 83% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryFeedViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryFeedViewModel.kt index d27acc98da..89355d0204 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryFeedViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryFeedViewModel.kt @@ -18,13 +18,15 @@ * 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.dvms.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.dvms.dal import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel +import kotlin.reflect.KClass @Stable class DvmContentDiscoveryFeedViewModel( @@ -38,6 +40,9 @@ class DvmContentDiscoveryFeedViewModel( val requestId: String, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = DvmContentDiscoveryFeedViewModel(account, dvmKey, requestId) as T + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = DvmContentDiscoveryFeedViewModel(account, dvmKey, requestId) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryResponseFilter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryResponseFilter.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryResponseFilter.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryResponseFilter.kt index c438c3ea8c..b11353ce50 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryResponseFilter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryResponseFilter.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.dvms.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.dvms.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/favorites/FavoriteAlgoFeedsListScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/favorites/FavoriteAlgoFeedsListScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/favorites/FavoriteAlgoFeedsListScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/favorites/FavoriteAlgoFeedsListScreen.kt index 6a6b8a143e..19a633532f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/favorites/FavoriteAlgoFeedsListScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/dvms/favorites/FavoriteAlgoFeedsListScreen.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.dvms.favorites +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.dvms.favorites import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable @@ -84,6 +84,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.note.elements.BannerImage import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.dvms.observeAppDefinition import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.dvms.rememberDvmHeartbeatFresh import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer @@ -92,7 +93,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SimpleImage35Modifier import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.observeAppDefinition import kotlinx.coroutines.launch private const val STAR_INLINE_ID = "star" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/AddEmojiDialog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/emojipacks/display/AddEmojiDialog.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/AddEmojiDialog.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/emojipacks/display/AddEmojiDialog.kt index 51c417b32e..2d688a396c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/AddEmojiDialog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/emojipacks/display/AddEmojiDialog.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.emojipacks.display +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.emojipacks.display import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -41,7 +41,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -56,10 +55,10 @@ import com.vitorpamplona.amethyst.commons.resources.emoji_public_explainer import com.vitorpamplona.amethyst.commons.resources.emoji_shortcode_invalid import com.vitorpamplona.amethyst.commons.resources.emoji_shortcode_label import com.vitorpamplona.amethyst.commons.resources.emoji_url_label +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectSingleFromGallery import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag @@ -119,7 +118,6 @@ fun AddEmojiDialog( onValueChange = { url = it }, label = { Text(stringRes(Res.string.emoji_url_label)) }, leadingIcon = { - val context = LocalContext.current SelectSingleFromGallery( isUploading = viewModel.isUploadingEmojiImage, tint = MaterialTheme.colorScheme.onSurfaceVariant, @@ -127,7 +125,7 @@ fun AddEmojiDialog( ) { selected -> viewModel.uploadEmojiImage( uri = selected, - context = context, + uploader = accountViewModel.host.mediaUploader, onUploaded = { uploadedUrl -> url = uploadedUrl }, onError = accountViewModel.toastManager::toast, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/emojipacks/display/EmojiPackScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/emojipacks/display/EmojiPackScreen.kt index 9fb240fd82..c8c628b4d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/display/EmojiPackScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/emojipacks/display/EmojiPackScreen.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.emojipacks.display +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.emojipacks.display import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -54,7 +54,6 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -68,6 +67,7 @@ import com.vitorpamplona.amethyst.commons.resources.quick_action_delete import com.vitorpamplona.amethyst.commons.ui.components.M3ActionDialog import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar @@ -84,7 +84,7 @@ fun EmojiPackScreen( nav: INav, ) { val viewModel: EmojiPackViewModel = - viewModel( + rememberViewModel( factory = EmojiPackViewModel.Initializer(accountViewModel.account, packIdentifier), ) EmojiPackScreenView(viewModel, accountViewModel, nav) diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/emojipacks/display/EmojiPackViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/emojipacks/display/EmojiPackViewModel.kt new file mode 100644 index 0000000000..99c20b5ea9 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/emojipacks/display/EmojiPackViewModel.kt @@ -0,0 +1,113 @@ +/* + * 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.commons.ui.screen.loggedIn.emojipacks.display + +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.CreationExtras +import com.vitorpamplona.amethyst.commons.model.Account +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer +import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlin.reflect.KClass + +@Stable +class EmojiPackViewModel( + val account: Account, + val packIdentifier: String, +) : ViewModel() { + val selectedPackFlow = + account.ownedEmojiPacks + .getOwnedEmojiPackFlow(packIdentifier) + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(2500), null) + + var isUploadingEmojiImage by mutableStateOf(false) + + suspend fun addEmoji( + emoji: EmojiUrlTag, + isPrivate: Boolean, + ) { + account.addEmojiToOwnedPack(packIdentifier, emoji, isPrivate) + } + + suspend fun removeEmoji( + shortcode: String, + isPrivate: Boolean, + ) { + account.removeEmojiFromOwnedPack(packIdentifier, shortcode, isPrivate) + } + + suspend fun deletePack() { + account.deleteOwnedEmojiPack(packIdentifier) + } + + fun uploadEmojiImage( + uri: SelectedMedia, + uploader: MediaUploader, + onUploaded: (String) -> Unit, + onError: (String, String) -> Unit, + ) { + viewModelScope.launch(Dispatchers.IO) { + upload( + uri, + uploader, + onUploading = { isUploadingEmojiImage = it }, + onUploaded = onUploaded, + onError = onError, + ) + } + } + + private suspend fun upload( + galleryUri: SelectedMedia, + uploader: MediaUploader, + onUploading: (Boolean) -> Unit, + onUploaded: (String) -> Unit, + onError: (String, String) -> Unit, + ) { + onUploading(true) + val url = uploadToDefaultServer(galleryUri, account, uploader, onError) + onUploading(false) + if (url != null) onUploaded(url) + } + + @Suppress("UNCHECKED_CAST") + class Initializer( + val account: Account, + val packIdentifier: String, + ) : ViewModelProvider.Factory { + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = EmojiPackViewModel(account, packIdentifier) as T + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataScreen.kt index 6c6bdcfa5b..99e38027f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataScreen.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.emojipacks.list.metadata +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.emojipacks.list.metadata import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -53,13 +53,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -71,6 +69,8 @@ import com.vitorpamplona.amethyst.commons.resources.emoji_pack_name_label import com.vitorpamplona.amethyst.commons.resources.emoji_pack_upload_image_cta import com.vitorpamplona.amethyst.commons.resources.emoji_pack_upload_image_hint import com.vitorpamplona.amethyst.commons.resources.new_emoji_pack +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.CreatingTopBar @@ -78,7 +78,6 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelectSingle @Composable fun EmojiPackMetadataScreen( @@ -86,7 +85,7 @@ fun EmojiPackMetadataScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val viewModel: EmojiPackMetadataViewModel = viewModel() + val viewModel: EmojiPackMetadataViewModel = rememberViewModel { EmojiPackMetadataViewModel() } viewModel.init(accountViewModel) if (packIdentifier != null) { @@ -109,7 +108,6 @@ private fun EmojiPackMetadataScaffold( accountViewModel: AccountViewModel, nav: INav, ) { - val context = LocalContext.current val scrollState = rememberScrollState() var wantsToPickImage by remember { mutableStateOf(false) } @@ -127,7 +125,7 @@ private fun EmojiPackMetadataScaffold( val onSubmit: () -> Unit = { viewModel.submit( - context = context, + uploader = accountViewModel.host.mediaUploader, onSuccess = { nav.popBack() }, onError = accountViewModel.toastManager::toast, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataViewModel.kt similarity index 52% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataViewModel.kt index 10a94b3a97..3047a3a289 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/emojipacks/list/metadata/EmojiPackMetadataViewModel.kt @@ -18,9 +18,8 @@ * 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.emojipacks.list.metadata +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.emojipacks.list.metadata -import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -29,32 +28,20 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Account -import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.OwnedEmojiPack import com.vitorpamplona.amethyst.commons.resources.Res -import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed -import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events -import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_upload -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title -import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled import com.vitorpamplona.amethyst.commons.resources.read_only_user -import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MetadataStripper -import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader -import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.launch -import kotlin.coroutines.cancellation.CancellationException @Stable class EmojiPackMetadataViewModel : ViewModel() { @@ -101,7 +88,7 @@ class EmojiPackMetadataViewModel : ViewModel() { } fun submit( - context: Context, + uploader: MediaUploader, onSuccess: () -> Unit, onError: (String, String) -> Unit, ) { @@ -111,7 +98,7 @@ class EmojiPackMetadataViewModel : ViewModel() { try { val local = pickedMedia if (local != null) { - val uploadedUrl = uploadImage(local, context, onError) + val uploadedUrl = uploadImage(local, uploader, onError) if (uploadedUrl == null) { isWorking = false return@launch @@ -165,81 +152,7 @@ class EmojiPackMetadataViewModel : ViewModel() { private suspend fun uploadImage( galleryUri: SelectedMedia, - context: Context, + uploader: MediaUploader, onError: (String, String) -> Unit, - ): String? { - val sourceUri = - try { - if (account.settings.stripLocationOnUpload) { - val result = MetadataStripper.strip(galleryUri.uri, galleryUri.mimeType, context.applicationContext) - if (!result.stripped) { - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.metadata_strip_failed_upload_cancelled), - ) - return null - } - result.uri - } else { - galleryUri.uri - } - } catch (e: AvifMetadataNotVerifiableException) { - onError( - loadStringRes(Res.string.metadata_strip_failed_title), - loadStringRes(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName), - ) - return null - } - val compResult = MediaCompressor().compress(sourceUri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext) - - return try { - val result = - if (account.settings.defaultFileServer.type == ServerType.NIP96) { - Nip96Uploader().upload( - uri = compResult.uri, - contentType = compResult.contentType, - size = compResult.size, - alt = null, - sensitiveContent = null, - serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, - onProgress = {}, - httpAuth = account::createHTTPAuthorization, - context = context, - ) - } else { - BlossomUploader().upload( - uri = compResult.uri, - contentType = compResult.contentType, - size = compResult.size, - alt = null, - sensitiveContent = null, - serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads, - httpAuth = account::createBlossomUploadAuth, - context = context, - ) - } - - if (result.url != null) { - result.url - } else { - onError( - loadStringRes(Res.string.failed_to_upload_media_no_details), - loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading), - ) - null - } - } catch (_: SignerExceptions.ReadOnlyException) { - onError( - loadStringRes(Res.string.failed_to_upload_media_no_details), - loadStringRes(Res.string.login_with_a_private_key_to_be_able_to_upload), - ) - null - } catch (e: Exception) { - if (e is CancellationException) throw e - onError(loadStringRes(Res.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName) - null - } - } + ): String? = uploadToDefaultServer(galleryUri, account, uploader, onError) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/FavoriteAppsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/favorites/FavoriteAppsScreen.kt similarity index 91% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/FavoriteAppsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/favorites/FavoriteAppsScreen.kt index a4c51199df..8c18fc8b4c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/FavoriteAppsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/favorites/FavoriteAppsScreen.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.favorites +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.favorites import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -54,15 +54,14 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.browser.OmniboxInput import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppIcon +import com.vitorpamplona.amethyst.commons.favorites.PreloadFavoriteNostrApps import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.navigation.Route @@ -73,11 +72,11 @@ import com.vitorpamplona.amethyst.commons.resources.favorite_apps import com.vitorpamplona.amethyst.commons.resources.favorite_apps_empty import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.platform.rememberAppLauncher +import com.vitorpamplona.amethyst.commons.ui.platform.rememberNappletIconModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher -import com.vitorpamplona.amethyst.favorites.PreloadFavoriteNostrApps -import com.vitorpamplona.amethyst.favorites.rememberNappletIconModel /** * The Favorite Apps grid: the user's pinned web clients / nsites / napplets as big launch buttons. @@ -91,9 +90,10 @@ fun FavoriteAppsScreen( accountViewModel: AccountViewModel, nav: INav, ) { + val appServices = LocalAppServices.current val appStillLoadingStr = stringRes(Res.string.favorite_app_still_loading) - val context = LocalContext.current - val apps by Amethyst.instance.favoriteApps.favorites + val appLauncher = rememberAppLauncher() + val apps by appServices.favoriteApps.favorites .collectAsStateWithLifecycle() // Fetch favorited nsite/napplet manifests up front so a tap launches immediately instead of showing @@ -125,8 +125,8 @@ fun FavoriteAppsScreen( } else { FavoriteAppsGrid( apps = apps, - onOpen = { FavoriteAppLauncher.launch(context, it, appStillLoadingStr) }, - onRemove = { Amethyst.instance.favoriteApps.remove(it.id) }, + onOpen = { appLauncher.launch(it, appStillLoadingStr) }, + onRemove = { appServices.favoriteApps.remove(it.id) }, modifier = Modifier .fillMaxSize() @@ -184,18 +184,19 @@ internal fun FavoriteAppCell( onOpen: () -> Unit, onRemove: () -> Unit, ) { + val appServices = LocalAppServices.current var menuOpen by remember { mutableStateOf(false) } // For a plain web favorite, prefer the favicon captured when its site was opened; an nsite/napplet uses // the verified icon blob bundled in its own content. Observing the key set recomputes the model as a // captured favicon arrives. - val iconKeys by Amethyst.instance.browserIcons.keys + val iconKeys by appServices.browserIcons.keys .collectAsStateWithLifecycle() val faviconModel = when (app) { is FavoriteApp.WebApp -> remember(app, iconKeys) { - OmniboxInput.hostOf(app.url)?.let(Amethyst.instance.browserIcons::iconModelFor) + OmniboxInput.hostOf(app.url)?.let(appServices.browserIcons::iconModelFor) } is FavoriteApp.NostrApp -> rememberNappletIconModel(app.coordinate) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksFeedLoaded.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/followPacks/list/FollowPacksFeedLoaded.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksFeedLoaded.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/followPacks/list/FollowPacksFeedLoaded.kt index 13090f0708..429826b943 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksFeedLoaded.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/followPacks/list/FollowPacksFeedLoaded.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.followPacks.list +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.followPacks.list import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Row @@ -34,10 +34,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.ChannelCardCompose import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.ChannelCardCompose import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent @OptIn(ExperimentalFoundationApi::class) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/followPacks/list/FollowPacksScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/followPacks/list/FollowPacksScreen.kt index 0043f6c6a7..88f4af5c29 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/followPacks/list/FollowPacksScreen.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.followPacks.list +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.followPacks.list import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -34,8 +34,8 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.followPacks.list.datasource.FollowPacksFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.list.datasource.FollowPacksFilterAssemblerSubscription @Composable fun FollowPacksScreen( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksTopBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/followPacks/list/FollowPacksTopBar.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksTopBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/followPacks/list/FollowPacksTopBar.kt index d50031d6a7..e7e0f59b0f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksTopBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/followPacks/list/FollowPacksTopBar.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.followPacks.list +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.followPacks.list import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/datasource/FollowPacksFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/followPacks/list/datasource/FollowPacksFilterAssemblerSubscription.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/datasource/FollowPacksFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/followPacks/list/datasource/FollowPacksFilterAssemblerSubscription.kt index 882b723143..910cb30a9f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/datasource/FollowPacksFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/followPacks/list/datasource/FollowPacksFilterAssemblerSubscription.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.followPacks.list.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.followPacks.list.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocachesHereFeedFilter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/dal/GeocachesHereFeedFilter.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocachesHereFeedFilter.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/dal/GeocachesHereFeedFilter.kt index eb4c2505b5..37aafe909f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocachesHereFeedFilter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/dal/GeocachesHereFeedFilter.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.geocaches.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.geocaches.dal.GeocachesFeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocachesHereFeedViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/dal/GeocachesHereFeedViewModel.kt similarity index 85% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocachesHereFeedViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/dal/GeocachesHereFeedViewModel.kt index c97d568fc3..51de6e3a4f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocachesHereFeedViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/dal/GeocachesHereFeedViewModel.kt @@ -18,13 +18,15 @@ * 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.geocaches.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.dal import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel +import kotlin.reflect.KClass /** * Drives the "N caches here" chip on a geohash screen. @@ -45,6 +47,9 @@ class GeocachesHereFeedViewModel( val account: Account, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = GeocachesHereFeedViewModel(geohash, account) as T + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = GeocachesHereFeedViewModel(geohash, account) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/log/LogGeocacheFindScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/log/LogGeocacheFindScreen.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/log/LogGeocacheFindScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/log/LogGeocacheFindScreen.kt index 12f69392be..a74c53ffda 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/log/LogGeocacheFindScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/log/LogGeocacheFindScreen.kt @@ -18,11 +18,8 @@ * 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.geocaches.log +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.log -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.PickVisualMediaRequest -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring @@ -57,16 +54,15 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res @@ -88,13 +84,15 @@ import com.vitorpamplona.amethyst.commons.resources.geocache_log_scan_code import com.vitorpamplona.amethyst.commons.resources.geocache_log_your_log import com.vitorpamplona.amethyst.commons.resources.geocache_unnamed import com.vitorpamplona.amethyst.commons.resources.route_log_geocache_find +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.rememberGeocachePalette import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.datasource.GeocachesFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner import com.vitorpamplona.quartz.nip01Core.core.Address import kotlinx.coroutines.launch @@ -122,9 +120,8 @@ fun LogGeocacheFindScreen( GeocachesFilterAssemblerSubscription(accountViewModel) val address = remember(kind, pubKeyHex, dTag) { Address(kind, pubKeyHex, dTag) } - val model: LogGeocacheFindViewModel = viewModel(key = "LogGeocacheFind-${address.toValue()}") + val model: LogGeocacheFindViewModel = rememberViewModel(key = "LogGeocacheFind-${address.toValue()}") { LogGeocacheFindViewModel() } val scope = rememberCoroutineScope() - val context = LocalContext.current remember(address) { model.init(accountViewModel, address) @@ -134,12 +131,15 @@ fun LogGeocacheFindScreen( val listing = model.listing() var scanning by remember { mutableStateOf(false) } - val photoPicker = - rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> - if (uri != null) { - scope.launch { model.uploadImage(uri, null, context) } + var pickingPhoto by rememberSaveable { mutableStateOf(false) } + if (pickingPhoto) { + GallerySelectSingle(imagesOnly = true) { media -> + pickingPhoto = false + if (media != null) { + scope.launch { model.uploadImage(media.uri, media.mimeType, accountViewModel.host.mediaUploader) } } } + } Scaffold( topBar = { @@ -221,7 +221,7 @@ fun LogGeocacheFindScreen( OutlinedButton( enabled = !model.isUploading.value, onClick = { - photoPicker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + pickingPhoto = true }, ) { if (model.isUploading.value) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/log/LogGeocacheFindViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/log/LogGeocacheFindViewModel.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/log/LogGeocacheFindViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/log/LogGeocacheFindViewModel.kt index 56711ae236..d41a08d49f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/log/LogGeocacheFindViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geocaches/log/LogGeocacheFindViewModel.kt @@ -18,19 +18,19 @@ * 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.geocaches.log +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.log -import android.content.Context -import android.net.Uri import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair @@ -183,9 +183,9 @@ class LogGeocacheFindViewModel : ViewModel() { } suspend fun uploadImage( - uri: Uri, + uri: MediaUri, mimeType: String?, - context: Context, + uploader: MediaUploader, ): Boolean { if (!::account.isInitialized) return false isUploading.value = true @@ -199,7 +199,7 @@ class LogGeocacheFindViewModel : ViewModel() { compressionQuality = CompressorQuality.MEDIUM, server = account.settings.defaultFileServer, account = account, - context = context, + uploader = uploader, ) val url = ((result as? UploadingState.Finished)?.result as? UploadOrchestrator.OrchestratorResult.ServerResult)?.url diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geohash/GeoHashScreen.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geohash/GeoHashScreen.kt index 1c42247bbd..5aca79bd14 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geohash/GeoHashScreen.kt @@ -18,16 +18,14 @@ * 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.geohash +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geohash -import android.annotation.SuppressLint import androidx.compose.foundation.layout.Column import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.profile.ui.FollowButton @@ -39,18 +37,19 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_unfollow import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.search.SearchSeed +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SearchTopBarAction import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName import com.vitorpamplona.amethyst.commons.ui.screen.RefresheableFeedView +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geohash.dal.GeoHashFeedViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geohash.datasource.GeoHashFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName -import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.dal.GeoHashFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource.GeoHashFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.icons.symbols.Icon as SymbolIcon @Composable @@ -64,7 +63,7 @@ fun GeoHashScreen( PrepareViewModelsGeoHashScreen(tag, accountViewModel, nav) } -@SuppressLint("StateFlowValueCalledInComposition") +@Suppress("StateFlowValueCalledInComposition") @Composable fun PrepareViewModelsGeoHashScreen( tag: Route.Geohash, @@ -72,7 +71,7 @@ fun PrepareViewModelsGeoHashScreen( nav: INav, ) { val geohashViewModel: GeoHashFeedViewModel = - viewModel( + rememberViewModel( key = tag.geohash + "GeoHashFeedViewModel", factory = GeoHashFeedViewModel.Factory( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeocachesHereChip.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geohash/GeocachesHereChip.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeocachesHereChip.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geohash/GeocachesHereChip.kt index 2bf49a987c..d35ab182b2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeocachesHereChip.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geohash/GeocachesHereChip.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.geohash +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geohash import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -37,7 +37,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -45,13 +44,14 @@ import com.vitorpamplona.amethyst.commons.model.navigation.GeocacheTab import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.geocache_caches_here +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.rememberGeocachePalette import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.dal.GeocachesHereFeedViewModel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.datasource.GeocachesFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.dal.GeocachesHereFeedViewModel /** * "N caches here", shown on a geohash feed when there are any. @@ -73,7 +73,7 @@ fun GeocachesHereChip( nav: INav, ) { val model: GeocachesHereFeedViewModel = - viewModel( + rememberViewModel( key = geohash + "GeocachesHereFeedViewModel", factory = GeocachesHereFeedViewModel.Factory(geohash, accountViewModel.account), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/NewGeoNoteButton.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geohash/NewGeoNoteButton.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/NewGeoNoteButton.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geohash/NewGeoNoteButton.kt index 8fa7a5f5e8..571c115e8a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/NewGeoNoteButton.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geohash/NewGeoNoteButton.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.geohash +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geohash import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.FloatingActionButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt index 88a241678f..5a4539892d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.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.geohash.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geohash.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.feeds.sortedByDefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geohash/dal/GeoHashFeedViewModel.kt similarity index 85% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geohash/dal/GeoHashFeedViewModel.kt index 4fd0ea3b81..9dc92f368e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geohash/dal/GeoHashFeedViewModel.kt @@ -18,15 +18,17 @@ * 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.geohash.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geohash.dal import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlin.reflect.KClass @Stable class GeoHashFeedViewModel( @@ -43,6 +45,9 @@ class GeoHashFeedViewModel( val account: Account, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = GeoHashFeedViewModel(geohash, relays, account) as T + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = GeoHashFeedViewModel(geohash, relays, account) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt index 18aa1cea55..eee6e40dc2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt @@ -18,9 +18,8 @@ * 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.geohash.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geohash.datasource -import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.commons.model.navigation.Route @@ -28,7 +27,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.geohash.GeohashQueryState import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -@SuppressLint("StateFlowValueCalledInComposition") +@Suppress("StateFlowValueCalledInComposition") @Composable fun GeoHashFilterAssemblerSubscription( tag: Route.Geohash, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagFeedLoaded.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/hashtag/HashtagFeedLoaded.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagFeedLoaded.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/hashtag/HashtagFeedLoaded.kt index ced407187e..954c75bd1d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagFeedLoaded.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/hashtag/HashtagFeedLoaded.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.hashtag +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/hashtag/HashtagScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/hashtag/HashtagScreen.kt index 9832596f06..f2348e55d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/hashtag/HashtagScreen.kt @@ -18,9 +18,8 @@ * 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.hashtag +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag -import android.annotation.SuppressLint import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -39,7 +38,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.navigation.Route @@ -57,6 +55,7 @@ import com.vitorpamplona.amethyst.commons.resources.mute_hashtag import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.resources.unmute_hashtag import com.vitorpamplona.amethyst.commons.search.SearchSeed +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded @@ -64,11 +63,11 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SearchTopBarAction import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.commons.ui.screen.RefresheableFeedView +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag.dal.HashtagFeedViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag.datasource.HashtagFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.StdPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.dal.HashtagFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource.HashtagFilterAssemblerSubscription @Composable fun HashtagScreen( @@ -81,7 +80,7 @@ fun HashtagScreen( PrepareViewModelsHashtagScreen(tag, accountViewModel, nav) } -@SuppressLint("StateFlowValueCalledInComposition") +@Suppress("StateFlowValueCalledInComposition") @Composable fun PrepareViewModelsHashtagScreen( tag: Route.Hashtag, @@ -89,7 +88,7 @@ fun PrepareViewModelsHashtagScreen( nav: INav, ) { val hashtagFeedViewModel: HashtagFeedViewModel = - viewModel( + rememberViewModel( key = tag.hashtag + "HashtagFeedViewModel", factory = HashtagFeedViewModel.Factory( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/NewHashtagNoteButton.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/hashtag/NewHashtagNoteButton.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/NewHashtagNoteButton.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/hashtag/NewHashtagNoteButton.kt index 97f7b7320a..5f9aac15b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/NewHashtagNoteButton.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/hashtag/NewHashtagNoteButton.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.hashtag +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.FloatingActionButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt index 3ecbdcc74f..65f5223e4d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.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.hashtag.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.feeds.isRenderableRepost diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/hashtag/dal/HashtagFeedViewModel.kt similarity index 85% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/hashtag/dal/HashtagFeedViewModel.kt index ddb32f8d8f..08c622343f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/hashtag/dal/HashtagFeedViewModel.kt @@ -18,15 +18,17 @@ * 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.hashtag.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag.dal import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlin.reflect.KClass @Stable class HashtagFeedViewModel( @@ -43,6 +45,9 @@ class HashtagFeedViewModel( val account: Account, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = HashtagFeedViewModel(hashtag, relays, account) as T + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = HashtagFeedViewModel(hashtag, relays, account) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt index 02660106a7..2425d5105e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.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.hashtag.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/PeopleListView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/display/PeopleListView.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/PeopleListView.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/display/PeopleListView.kt index ea29830159..a02a03d243 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/PeopleListView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/display/PeopleListView.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.lists.display +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.display import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/ShowUserSuggestions.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/display/ShowUserSuggestions.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/ShowUserSuggestions.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/display/ShowUserSuggestions.kt index 585ec8d72b..7a8c628816 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/ShowUserSuggestions.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/display/ShowUserSuggestions.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.lists.display +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.display import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -85,6 +85,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt index 6dcb4548ed..e9396f3bc2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/display/lists/PeopleListScreen.kt @@ -18,9 +18,8 @@ * 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.lists.display.lists +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.display.lists -import android.content.Intent import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Column @@ -59,13 +58,11 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.User @@ -87,6 +84,8 @@ import com.vitorpamplona.amethyst.commons.ui.components.ClickableBox import com.vitorpamplona.amethyst.commons.ui.components.M3ActionDialog import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection +import com.vitorpamplona.amethyst.commons.ui.components.rememberTextSharer +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -94,6 +93,9 @@ import com.vitorpamplona.amethyst.commons.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.commons.ui.note.ClearTextIcon import com.vitorpamplona.amethyst.commons.ui.note.VerticalDotsIcon import com.vitorpamplona.amethyst.commons.ui.note.externalLinkForNote +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.display.DrawUser +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.display.PeopleListView +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.display.RenderAddUserFieldAndSuggestions import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness @@ -105,9 +107,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.TabRowHeight import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.DrawUser -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.PeopleListView -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.RenderAddUserFieldAndSuggestions import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @@ -119,7 +118,7 @@ fun PeopleListScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val viewModel: PeopleListViewModel = viewModel() + val viewModel: PeopleListViewModel = rememberViewModel { PeopleListViewModel() } viewModel.init(accountViewModel, selectedDTag) val pagerState = rememberPagerState { 2 } @@ -433,30 +432,14 @@ private fun ListActionsMenuButton( } if (isActionListOpen.value) { - val context = LocalContext.current + val textSharer = rememberTextSharer() M3ActionDialog( title = stringRes(Res.string.list_actions_dialog_title), onDismiss = { isActionListOpen.value = false }, ) { M3ActionSection { M3ActionRow(icon = MaterialSymbols.Share, text = stringRes(Res.string.quick_action_share)) { - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra( - Intent.EXTRA_TEXT, - externalLinkForNote(note()), - ) - putExtra( - Intent.EXTRA_TITLE, - quickActionShareBrowserLinkStr, - ) - } - - val shareIntent = - Intent.createChooser(sendIntent, quickActionShareStr) - context.startActivity(shareIntent) + textSharer.share(externalLinkForNote(note()), quickActionShareBrowserLinkStr, quickActionShareStr) isActionListOpen.value = false } M3ActionRow(icon = MaterialSymbols.Edit, text = stringRes(Res.string.follow_set_edit_list_metadata)) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt index 15ed464437..bd5d8ed5f3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.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.lists.display.lists +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.display.lists import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue @@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt similarity index 91% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt index 633661c400..c6b42a6d50 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/display/packs/FollowPackScreen.kt @@ -18,9 +18,8 @@ * 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.lists.display.packs +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.display.packs -import android.content.Intent import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Column @@ -52,13 +51,11 @@ 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.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.User @@ -76,6 +73,8 @@ import com.vitorpamplona.amethyst.commons.ui.components.ClickableBox import com.vitorpamplona.amethyst.commons.ui.components.M3ActionDialog import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection +import com.vitorpamplona.amethyst.commons.ui.components.rememberTextSharer +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -83,6 +82,9 @@ import com.vitorpamplona.amethyst.commons.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.commons.ui.note.ClearTextIcon import com.vitorpamplona.amethyst.commons.ui.note.VerticalDotsIcon import com.vitorpamplona.amethyst.commons.ui.note.externalLinkForNote +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.display.DrawUser +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.display.PeopleListView +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.display.RenderAddUserFieldAndSuggestions import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness @@ -93,9 +95,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdPadding import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.DrawUser -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.PeopleListView -import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.RenderAddUserFieldAndSuggestions import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow @@ -106,7 +105,7 @@ fun FollowPackScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val viewModel: FollowPackViewModel = viewModel() + val viewModel: FollowPackViewModel = rememberViewModel { FollowPackViewModel() } viewModel.init(accountViewModel, selectedDTag) Scaffold( @@ -284,30 +283,14 @@ private fun ListActionsMenuButton( } if (isActionListOpen.value) { - val context = LocalContext.current + val textSharer = rememberTextSharer() M3ActionDialog( title = stringRes(Res.string.pack_actions_dialog_title), onDismiss = { isActionListOpen.value = false }, ) { M3ActionSection { M3ActionRow(icon = MaterialSymbols.Share, text = stringRes(Res.string.quick_action_share)) { - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra( - Intent.EXTRA_TEXT, - externalLinkForNote(note()), - ) - putExtra( - Intent.EXTRA_TITLE, - quickActionShareBrowserLinkStr, - ) - } - - val shareIntent = - Intent.createChooser(sendIntent, quickActionShareStr) - context.startActivity(shareIntent) + textSharer.share(externalLinkForNote(note()), quickActionShareBrowserLinkStr, quickActionShareStr) isActionListOpen.value = false } M3ActionRow(icon = MaterialSymbols.Edit, text = stringRes(Res.string.follow_pack_edit_list_metadata)) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt index cf7e4f725b..922843cab4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.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.lists.display.packs +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.lists.display.packs import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue @@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/livestreams/LiveStreamsFeedLoaded.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/livestreams/LiveStreamsFeedLoaded.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/livestreams/LiveStreamsFeedLoaded.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/livestreams/LiveStreamsFeedLoaded.kt index 3d9f4368ff..489b03139c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/livestreams/LiveStreamsFeedLoaded.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/livestreams/LiveStreamsFeedLoaded.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.livestreams +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.livestreams import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Row @@ -34,10 +34,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.ChannelCardCompose import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.ChannelCardCompose import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent @OptIn(ExperimentalFoundationApi::class) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/livestreams/LiveStreamsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/livestreams/LiveStreamsScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/livestreams/LiveStreamsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/livestreams/LiveStreamsScreen.kt index 370f855c61..b811e5ed08 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/livestreams/LiveStreamsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/livestreams/LiveStreamsScreen.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.livestreams +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.livestreams import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -34,8 +34,8 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.livestreams.datasource.LiveStreamsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.datasource.LiveStreamsFilterAssemblerSubscription @Composable fun LiveStreamsScreen( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/livestreams/LiveStreamsTopBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/livestreams/LiveStreamsTopBar.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/livestreams/LiveStreamsTopBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/livestreams/LiveStreamsTopBar.kt index c0254eb04b..fec93abbaa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/livestreams/LiveStreamsTopBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/livestreams/LiveStreamsTopBar.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.livestreams +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.livestreams import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/livestreams/datasource/LiveStreamsFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/livestreams/datasource/LiveStreamsFilterAssemblerSubscription.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/livestreams/datasource/LiveStreamsFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/livestreams/datasource/LiveStreamsFilterAssemblerSubscription.kt index 0b8870df4c..67a82ceba1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/livestreams/datasource/LiveStreamsFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/livestreams/datasource/LiveStreamsFilterAssemblerSubscription.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.livestreams.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.livestreams.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicComposerUploadUi.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/music/MusicComposerUploadUi.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicComposerUploadUi.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/music/MusicComposerUploadUi.kt index 7894e15e7e..4024ff1ad1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/MusicComposerUploadUi.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/music/MusicComposerUploadUi.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.music +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -50,10 +50,10 @@ import coil3.compose.rememberAsyncImagePainter import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.ShowImageUploadGallery import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery import org.jetbrains.compose.resources.StringResource /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt index b5b837f382..17e51187ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/music/NewMusicPlaylistScreen.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.music +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -57,13 +57,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -94,7 +92,9 @@ import com.vitorpamplona.amethyst.commons.resources.music_playlist_unknown_track import com.vitorpamplona.amethyst.commons.resources.music_playlist_uploading_banner import com.vitorpamplona.amethyst.commons.resources.new_music_playlist import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -105,7 +105,6 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent import kotlinx.collections.immutable.persistentListOf @@ -115,8 +114,7 @@ fun NewMusicPlaylistScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val vm: NewMusicPlaylistViewModel = viewModel() - val context = LocalContext.current + val vm: NewMusicPlaylistViewModel = rememberViewModel { NewMusicPlaylistViewModel() } LaunchedEffect(accountViewModel) { vm.init(accountViewModel, editDTag) @@ -155,7 +153,6 @@ fun NewMusicPlaylistScreen( onPost = { if (!vm.isValid() || isBusy) return@SendingTopBar vm.saveAndPublish( - context = context, accountViewModel = accountViewModel, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/music/NewMusicPlaylistViewModel.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/music/NewMusicPlaylistViewModel.kt index 179566e82a..fbc2db14f3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/music/NewMusicPlaylistViewModel.kt @@ -18,23 +18,23 @@ * 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.music +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music -import android.content.Context import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent import com.vitorpamplona.quartz.nip01Core.core.Address import kotlinx.collections.immutable.ImmutableList @@ -184,10 +184,7 @@ class NewMusicPlaylistViewModel : ViewModel() { * runs on [AccountViewModel.viewModelScope] via `launchSigner`, so it keeps running even if * the user leaves the screen — they get a toast notification when it finishes either way. */ - fun saveAndPublish( - context: Context, - accountViewModel: AccountViewModel, - ) { + fun saveAndPublish(accountViewModel: AccountViewModel) { if (isSending.value) return // double-tap guard if (!isValid()) return @@ -214,10 +211,10 @@ class NewMusicPlaylistViewModel : ViewModel() { coverOrchestrator = coverMedia.value, existingCoverUrl = coverUrl.value.trim().ifBlank { null }, server = server, - quality = MediaCompressor.intToCompressorQuality(mediaQualitySlider.value), + quality = CompressorQuality.fromSlider(mediaQualitySlider.value), stripMetadata = stripMetadata.value, loadedEvent = loadedEvent, - appContext = context.applicationContext, + uploader = accountViewModel.host.mediaUploader, ) isSending.value = true @@ -243,7 +240,7 @@ class NewMusicPlaylistViewModel : ViewModel() { } catch (t: Throwable) { accountViewModel.toastManager.toast( "Failed to send playlist", - t.message ?: t.javaClass.simpleName, + t.message ?: t::class.simpleName ?: "", ) } finally { withContext(Dispatchers.Main.immediate) { isSending.value = false } @@ -264,7 +261,7 @@ class NewMusicPlaylistViewModel : ViewModel() { val quality: CompressorQuality, val stripMetadata: Boolean, val loadedEvent: MusicPlaylistEvent?, - val appContext: Context, + val uploader: MediaUploader, ) private suspend fun runCoverUpload( @@ -279,13 +276,13 @@ class NewMusicPlaylistViewModel : ViewModel() { mediaQuality = snapshot.quality, server = server, account = account, - context = snapshot.appContext, + uploader = snapshot.uploader, useH265 = false, stripMetadata = snapshot.stripMetadata, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) if (!res.allGood) { - throw UploadException(formatUploadErrors(res.errors, snapshot.appContext)) + throw UploadException(formatUploadErrors(res.errors)) } return firstUploadedUrl(res.successful) ?: throw UploadException("Server didn't return a URL for the uploaded cover.") @@ -300,10 +297,7 @@ class NewMusicPlaylistViewModel : ViewModel() { .firstNotNullOfOrNull { it.result as? UploadOrchestrator.OrchestratorResult.ServerResult } ?.url - private suspend fun formatUploadErrors( - errors: List, - context: Context, - ): String = + private suspend fun formatUploadErrors(errors: List): String = errors .map { loadStringRes(it.errorResource, *it.params) } .distinct() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/napplets/ConnectedAppsScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/napplets/ConnectedAppsScreen.kt index 25c7625df0..e82e70f476 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/napplets/ConnectedAppsScreen.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.napplets +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.napplets import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -52,7 +52,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46PermissionAuthorizer import com.vitorpamplona.amethyst.commons.connectedApps.signers.AppSignerPolicy import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionLedger @@ -69,13 +68,15 @@ import com.vitorpamplona.amethyst.commons.resources.napplet_permissions_title import com.vitorpamplona.amethyst.commons.resources.napplet_policy_full_trust import com.vitorpamplona.amethyst.commons.resources.napplet_policy_paranoid import com.vitorpamplona.amethyst.commons.resources.napplet_policy_reasonable +import com.vitorpamplona.amethyst.commons.resources.napplet_untitled import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.platform.rememberManifestIconModel +import com.vitorpamplona.amethyst.commons.ui.platform.rememberWebAppIconModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.napplets.datasource.ConnectedAppsFilterAssemblerSubscription +import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.favorites.rememberManifestIconModel -import com.vitorpamplona.amethyst.favorites.rememberWebAppIconModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.datasource.ConnectedAppsFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip19Bech32.entities.NPub @@ -86,7 +87,6 @@ import com.vitorpamplona.quartz.nip5dNapplets.NappletManifest import com.vitorpamplona.quartz.nip5dNapplets.RootNappletEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import com.vitorpamplona.amethyst.commons.R as CommonsR /** Author placeholder used by the browser permission path — not a real pubkey. */ private const val BROWSER_AUTHOR = "browser" @@ -101,8 +101,9 @@ fun ConnectedAppsScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val capabilityLedger = Amethyst.instance.nappletPermissionLedger - val signerLedger = remember { NostrSignerPermissionLedger(Amethyst.instance.signerPermissionStore) } + val appServices = LocalAppServices.current + val capabilityLedger = appServices.nappletPermissionLedger + val signerLedger = remember { NostrSignerPermissionLedger(appServices.signerPermissionStore) } var items by remember { mutableStateOf?>(null) } var nappletAuthors by remember { mutableStateOf>(emptySet()) } @@ -156,7 +157,7 @@ fun ConnectedAppsScreen( } else -> { - val untitled = stringRes(CommonsR.string.napplet_untitled) + val untitled = stringRes(Res.string.napplet_untitled) LazyColumn( modifier = Modifier.fillMaxSize().padding(padding), contentPadding = PaddingValues(16.dp), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/datasource/ConnectedAppsFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/napplets/datasource/ConnectedAppsFilterAssemblerSubscription.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/datasource/ConnectedAppsFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/napplets/datasource/ConnectedAppsFilterAssemblerSubscription.kt index c6a357b9a5..df4c549108 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/datasource/ConnectedAppsFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/napplets/datasource/ConnectedAppsFilterAssemblerSubscription.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.napplets.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.napplets.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/datasource/NappletsFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/napplets/datasource/NappletsFilterAssemblerSubscription.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/datasource/NappletsFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/napplets/datasource/NappletsFilterAssemblerSubscription.kt index f2af75c0db..063aadf0dd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/datasource/NappletsFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/napplets/datasource/NappletsFilterAssemblerSubscription.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.napplets.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.napplets.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt index 47aefaed52..f758022943 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.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.podcasts.authoring +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -44,11 +44,9 @@ import androidx.compose.runtime.remember 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.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.podcast_edit_show import com.vitorpamplona.amethyst.commons.resources.podcast_publishing_banner @@ -74,14 +72,15 @@ import com.vitorpamplona.amethyst.commons.resources.podcast_show_type_label import com.vitorpamplona.amethyst.commons.resources.podcast_show_type_serial import com.vitorpamplona.amethyst.commons.resources.podcast_show_website_label import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SendingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.CoverImagePicker +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.UploadInProgressBanner +import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelectSingle -import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.CoverImagePicker -import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.UploadInProgressBanner -import com.vitorpamplona.amethyst.ui.stringRes import kotlinx.collections.immutable.persistentListOf import org.jetbrains.compose.resources.StringResource @@ -96,8 +95,7 @@ fun EditPodcastShowScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val vm: EditPodcastShowViewModel = viewModel() - val context = LocalContext.current + val vm: EditPodcastShowViewModel = rememberViewModel { EditPodcastShowViewModel() } LaunchedEffect(accountViewModel) { vm.init(accountViewModel) } @@ -124,7 +122,7 @@ fun EditPodcastShowScreen( isActive = { vm.isValid() && !isBusy }, onPost = { if (!vm.isValid() || isBusy) return@SendingTopBar - vm.saveAndPublish(context, accountViewModel) + vm.saveAndPublish(accountViewModel) }, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt index d5f77f1e98..bf30181a6f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt @@ -18,9 +18,8 @@ * 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.podcasts.authoring +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring -import android.content.Context import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.Account @@ -28,10 +27,11 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.podcasts.V4VSplitEditorState import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nipXXPodcasting20.metadata.Podcasting20PodcastMetadata @@ -129,10 +129,7 @@ class EditPodcastShowViewModel : ViewModel() { fun isValid(): Boolean = title.value.isNotBlank() - fun saveAndPublish( - context: Context, - accountViewModel: AccountViewModel, - ) { + fun saveAndPublish(accountViewModel: AccountViewModel) { if (isSending.value) return val coverOrch = coverMedia.value @@ -168,9 +165,9 @@ class EditPodcastShowViewModel : ViewModel() { ), coverOrchestrator = coverOrch, server = server, - quality = MediaCompressor.intToCompressorQuality(mediaQualitySlider.value), + quality = CompressorQuality.fromSlider(mediaQualitySlider.value), stripMetadata = stripMetadata.value, - appContext = context.applicationContext, + uploader = accountViewModel.host.mediaUploader, ) isSending.value = true @@ -186,7 +183,7 @@ class EditPodcastShowViewModel : ViewModel() { quality = snapshot.quality, stripMetadata = snapshot.stripMetadata, alt = snapshot.content.title, - context = snapshot.appContext, + uploader = snapshot.uploader, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) } @@ -207,7 +204,7 @@ class EditPodcastShowViewModel : ViewModel() { } catch (t: Throwable) { accountViewModel.toastManager.toast( "Failed to save podcast", - t.message ?: t.javaClass.simpleName, + t.message ?: t::class.simpleName ?: "", ) } finally { withContext(Dispatchers.Main.immediate) { isSending.value = false } @@ -219,9 +216,9 @@ class EditPodcastShowViewModel : ViewModel() { val content: Podcasting20PodcastMetadata.Content, val coverOrchestrator: MultiOrchestrator?, val server: ServerName?, - val quality: com.vitorpamplona.amethyst.service.uploads.CompressorQuality, + val quality: CompressorQuality, val stripMetadata: Boolean, - val appContext: Context, + val uploader: MediaUploader, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerScreen.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerScreen.kt index 81f617a207..cc3a236e0c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerScreen.kt @@ -18,11 +18,8 @@ * 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.podcasts.authoring +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring -import android.net.Uri -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -50,12 +47,10 @@ import androidx.compose.runtime.remember 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.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res @@ -71,14 +66,16 @@ import com.vitorpamplona.amethyst.commons.resources.podcast_trailer_upload_cta import com.vitorpamplona.amethyst.commons.resources.podcast_trailer_upload_hint import com.vitorpamplona.amethyst.commons.resources.podcast_trailer_url_label import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.DocumentSelectSingle +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SendingTopBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.UploadInProgressBanner +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.music.UploadPlaceholder import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.UploadInProgressBanner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.UploadPlaceholder /** Composer for a Podcasting-2.0 trailer (`kind:30055`): title, a short audio/video clip, and season. */ @OptIn(ExperimentalMaterial3Api::class) @@ -87,8 +84,7 @@ fun NewPodcastTrailerScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val vm: NewPodcastTrailerViewModel = viewModel() - val context = LocalContext.current + val vm: NewPodcastTrailerViewModel = rememberViewModel { NewPodcastTrailerViewModel() } LaunchedEffect(accountViewModel) { vm.init(accountViewModel) } StrippingFailureDialog(vm.strippingFailureConfirmation) @@ -112,7 +108,7 @@ fun NewPodcastTrailerScreen( isActive = { vm.isValid() && !isBusy }, onPost = { if (!vm.isValid() || isBusy) return@SendingTopBar - vm.saveAndPublish(context, accountViewModel) + vm.saveAndPublish(accountViewModel) }, ) }, @@ -202,14 +198,6 @@ fun NewPodcastTrailerScreen( } } -// Audio or video file via OpenDocument. +// Audio or video file via the document picker. @Composable -private fun MediaFileSelect(onPicked: (SelectedMedia?) -> Unit) { - val resolver = LocalContext.current.contentResolver - val launcher = - rememberLauncherForActivityResult( - contract = ActivityResultContracts.OpenDocument(), - onResult = { uri: Uri? -> onPicked(uri?.let { SelectedMedia(it, resolver.getType(it)) }) }, - ) - LaunchedEffect(Unit) { launcher.launch(arrayOf("audio/*", "video/*")) } -} +private fun MediaFileSelect(onPicked: (SelectedMedia?) -> Unit) = DocumentSelectSingle(listOf("audio/*", "video/*"), onPicked) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerViewModel.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerViewModel.kt index 19ad47e7cf..d9b5a530a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerViewModel.kt @@ -18,18 +18,18 @@ * 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.podcasts.authoring +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring -import android.content.Context import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.lastPathSegmentOrNull import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.quartz.nipXXPodcasting20.trailer.Podcasting20TrailerEvent import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers @@ -79,7 +79,7 @@ class NewPodcastTrailerViewModel : ViewModel() { return } media.value = MultiOrchestrator(persistentListOf(uri)) - pickedName.value = uri.uri.lastPathSegment?.substringAfterLast('/') + pickedName.value = uri.uri.lastPathSegmentOrNull()?.substringAfterLast('/') pickedMimeType.value = uri.mimeType } @@ -91,10 +91,7 @@ class NewPodcastTrailerViewModel : ViewModel() { fun isValid(): Boolean = title.value.isNotBlank() && (media.value != null || url.value.isNotBlank()) - fun saveAndPublish( - context: Context, - accountViewModel: AccountViewModel, - ) { + fun saveAndPublish(accountViewModel: AccountViewModel) { if (isSending.value) return val server = selectedServer.value if (server == null) { @@ -110,9 +107,8 @@ class NewPodcastTrailerViewModel : ViewModel() { val seasonSnap = season.value.trim().toIntOrNull() val mimeSnap = pickedMimeType.value val mediaSnap = media.value - val quality = MediaCompressor.intToCompressorQuality(mediaQualitySlider.value) + val quality = CompressorQuality.fromSlider(mediaQualitySlider.value) val strip = stripMetadata.value - val appContext = context.applicationContext isSending.value = true accountViewModel.launchSigner { @@ -127,7 +123,7 @@ class NewPodcastTrailerViewModel : ViewModel() { quality = quality, stripMetadata = strip, alt = titleSnap.ifBlank { null }, - context = appContext, + uploader = accountViewModel.host.mediaUploader, onStrippingFailed = strippingFailureConfirmation::awaitConfirmation, ) } else { @@ -154,7 +150,7 @@ class NewPodcastTrailerViewModel : ViewModel() { _completionEvents.tryEmit(Unit) } catch (t: Throwable) { - accountViewModel.toastManager.toast("Failed to publish trailer", t.message ?: t.javaClass.simpleName) + accountViewModel.toastManager.toast("Failed to publish trailer", t.message ?: t::class.simpleName ?: "") } finally { withContext(Dispatchers.Main.immediate) { isSending.value = false } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/PodcastComposerMedia.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/PodcastComposerMedia.kt similarity index 65% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/PodcastComposerMedia.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/PodcastComposerMedia.kt index 5198d8b943..014e23554a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/PodcastComposerMedia.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/PodcastComposerMedia.kt @@ -18,22 +18,21 @@ * 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.podcasts.authoring +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring -import android.content.Context -import android.media.MediaMetadataRetriever -import android.net.Uri import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName +import com.vitorpamplona.amethyst.commons.podcasts.rfc2822Date +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.ui.loadStringRes -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState -import java.time.ZoneId -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter -import java.util.UUID +import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid /** * Shared upload + media-probe mechanics for the Podcasting-2.0 composers (episode, show, trailer). @@ -58,7 +57,7 @@ object PodcastComposerMedia { quality: CompressorQuality, stripMetadata: Boolean, alt: String?, - context: Context, + uploader: MediaUploader, onStrippingFailed: suspend () -> Boolean, ): String { val res = @@ -68,12 +67,12 @@ object PodcastComposerMedia { mediaQuality = quality, server = server, account = account, - context = context, + uploader = uploader, useH265 = false, stripMetadata = stripMetadata, onStrippingFailed = onStrippingFailed, ) - if (!res.allGood) throw UploadException(kind, formatUploadErrors(res.errors, context)) + if (!res.allGood) throw UploadException(kind, formatUploadErrors(res.errors)) return firstUploadedUrl(res.successful) ?: throw UploadException(kind, "Server didn't return a URL for the uploaded $kind.") } @@ -83,10 +82,7 @@ object PodcastComposerMedia { .firstNotNullOfOrNull { it.result as? UploadOrchestrator.OrchestratorResult.ServerResult } ?.url - private suspend fun formatUploadErrors( - errors: List, - context: Context, - ): String = + private suspend fun formatUploadErrors(errors: List): String = errors .map { loadStringRes(it.errorResource, *it.params) } .distinct() @@ -98,34 +94,12 @@ object PodcastComposerMedia { val title: String?, ) - /** - * Reads duration + title from a picked audio file via [MediaMetadataRetriever] (heavy — call off - * the main thread). Returns null if the provider rejects it or the file isn't a real container. - */ - fun probeAudio( - context: Context, - uri: Uri, - ): ProbedAudio? { - val retriever = MediaMetadataRetriever() - return try { - retriever.setDataSource(context, uri) - val durationMs = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull() - ProbedAudio( - durationSeconds = durationMs?.let { (it / 1000).toInt().takeIf { secs -> secs > 0 } }, - title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)?.trim()?.ifBlank { null }, - ) - } catch (_: Exception) { - null - } finally { - retriever.release() - } - } - /** Current time as an RFC2822 date string (`Tue, 24 Jun 2025 12:00:00 GMT`) — the spec's `pubdate`. */ - fun rfc2822Now(): String = DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(ZoneId.of("GMT"))) + fun rfc2822Now(): String = rfc2822Date(TimeUtils.now()) /** A fresh, stable `d` tag for a new addressable episode/trailer. */ - fun generateDTag(prefix: String): String = "$prefix-${System.currentTimeMillis() / 1000}-${UUID.randomUUID().toString().take(8)}" + @OptIn(ExperimentalUuidApi::class) + fun generateDTag(prefix: String): String = "$prefix-${TimeUtils.now()}-${Uuid.random().toString().take(8)}" /** Splits a comma-separated text field into a clean list (trimmed, no blanks). */ fun parseCsv(text: String): List = text.split(',').map { it.trim() }.filter { it.isNotEmpty() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/V4VSplitEditor.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/V4VSplitEditor.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/V4VSplitEditor.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/V4VSplitEditor.kt index 13be6b657e..ecdbe4a50e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/V4VSplitEditor.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/podcasts/authoring/V4VSplitEditor.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.podcasts.authoring +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt index 5ecce348ac..3810d2f146 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.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.privacy +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.privacy import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically @@ -54,7 +54,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -103,15 +102,16 @@ import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SegmentedChoiceTile +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsBlockTile +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsControlRow +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsDivider +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsSection +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsSwitchTile +import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SegmentedChoiceTile -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsBlockTile -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsControlRow -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsDivider -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSection -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSwitchTile -import com.vitorpamplona.amethyst.ui.stringRes import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import org.jetbrains.compose.resources.StringResource @@ -119,7 +119,7 @@ import org.jetbrains.compose.resources.StringResource @OptIn(ExperimentalMaterial3Api::class) @Composable fun PrivacyOptionsScreen(nav: INav) { - PrivacyOptionsScreen(Amethyst.instance.torPrefs.value, nav) + PrivacyOptionsScreen(LocalAppServices.current.torSettings, nav) } @OptIn(ExperimentalMaterial3Api::class) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsFeedLoaded.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/products/ProductsFeedLoaded.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsFeedLoaded.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/products/ProductsFeedLoaded.kt index c8c8f0d7d8..43e921e9a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsFeedLoaded.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/products/ProductsFeedLoaded.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.products +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.products import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi @@ -42,10 +42,10 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.commons.ui.feeds.PrefetchLoadedFeedMedia import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.ChannelCardCompose import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.ChannelCardCompose import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/products/ProductsScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/products/ProductsScreen.kt index 83a312fd5f..00d233df05 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/products/ProductsScreen.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.products +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.products import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -35,8 +35,8 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.products.datasource.ProductsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.products.datasource.ProductsFilterAssemblerSubscription @Composable fun ProductsScreen( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsTopBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/products/ProductsTopBar.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsTopBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/products/ProductsTopBar.kt index 6e7ff0a6d9..d377522fec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/ProductsTopBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/products/ProductsTopBar.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.products +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.products import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/datasource/ProductsFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/products/datasource/ProductsFilterAssemblerSubscription.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/datasource/ProductsFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/products/datasource/ProductsFilterAssemblerSubscription.kt index f5c0c2b7f5..929867fef4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/datasource/ProductsFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/products/datasource/ProductsFilterAssemblerSubscription.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.products.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.products.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt index 9190b18848..898b31b9c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.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.publicChats +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.publicChats import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -55,11 +55,11 @@ import com.vitorpamplona.amethyst.commons.ui.note.ClickableNote import com.vitorpamplona.amethyst.commons.ui.note.LongPressToQuickAction import com.vitorpamplona.amethyst.commons.ui.note.WatchNoteEvent import com.vitorpamplona.amethyst.commons.ui.note.calculateBackgroundColor +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.nip28Chats.RenderPublicChatChannelThumb import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdPadding import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.RenderPublicChatChannelThumb @Composable fun PublicChatsFeedLoaded( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/publicChats/PublicChatsScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/publicChats/PublicChatsScreen.kt index 66bb155ece..abf099c713 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/publicChats/PublicChatsScreen.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.publicChats +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.publicChats import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -36,8 +36,8 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.publicChats.datasource.PublicChatsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.publicChats.datasource.PublicChatsFilterAssemblerSubscription @Composable fun PublicChatsScreen( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsTopBar.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/publicChats/PublicChatsTopBar.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsTopBar.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/publicChats/PublicChatsTopBar.kt index 423d0517c0..e960e978e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsTopBar.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/publicChats/PublicChatsTopBar.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.publicChats +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.publicChats import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/datasource/PublicChatsFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/publicChats/datasource/PublicChatsFilterAssemblerSubscription.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/datasource/PublicChatsFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/publicChats/datasource/PublicChatsFilterAssemblerSubscription.kt index 14be5c7925..106cdab8f0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/datasource/PublicChatsFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/publicChats/datasource/PublicChatsFilterAssemblerSubscription.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.publicChats.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.publicChats.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.kt new file mode 100644 index 0000000000..f87c641378 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.kt @@ -0,0 +1,34 @@ +/* + * 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.commons.ui.screen.loggedIn.qrcode + +import androidx.compose.runtime.Composable + +/** + * Pins the screen to full brightness, and awake, while a QR code is on it. + * + * Half of "this QR code will not scan" is the phone showing it. A dark-mode OLED at the + * auto-brightness the room asked for can put so little contrast between the black and white + * modules that the other camera's binarizer cannot separate them — and the person holding it has + * no idea that is the problem, because to a human eye the code looks perfectly clear. + */ +@Composable +expect fun KeepScreenBrightAndAwake() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/QrCodeDrawer.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrCodeDrawer.kt similarity index 78% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/QrCodeDrawer.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrCodeDrawer.kt index faf17efbf8..d9de803a77 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/QrCodeDrawer.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrCodeDrawer.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.qrcode +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode import androidx.compose.foundation.Canvas import androidx.compose.foundation.background @@ -42,11 +42,6 @@ import androidx.compose.ui.graphics.PathFillType import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.google.zxing.EncodeHintType -import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel -import com.google.zxing.qrcode.encoder.ByteMatrix -import com.google.zxing.qrcode.encoder.Encoder -import com.google.zxing.qrcode.encoder.QRCode import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder import kotlin.math.min @@ -79,7 +74,7 @@ fun QrCodeDrawer( contents: String, modifier: Modifier = Modifier, ) { - val qrCode = remember(contents) { createQrCode(contents = contents) } + val matrix = remember(contents) { encodeQrMatrix(contents) } val foregroundColor = Color.Black @@ -91,64 +86,60 @@ fun QrCodeDrawer( .aspectRatio(1f) .background(Color.White), ) { - Canvas(modifier = Modifier.fillMaxSize()) { - // Calculate the height and width of each column/row - // Solve for the module size with the quiet zone measured in modules, so the whole code - // (zone included) is exactly as wide as the canvas. - val rowHeight = size.height / (qrCode.matrix.height + QR_QUIET_ZONE_MODULES * 2f) - val columnWidth = size.width / (qrCode.matrix.width + QR_QUIET_ZONE_MODULES * 2f) - // Scale the rounding with the module size. A fixed 20px radius is a gentle touch on - // a large code and a serious deformation on a small one: the finder patterns are what - // a decoder locates first, and rounding away a third of a module's worth of their - // corners is exactly the kind of damage that makes a code readable on screen and - // unreadable in a photo of that screen. - val radius = CornerRadius(min(columnWidth, rowHeight) * FINDER_CORNER_RADIUS_MODULES) - - // Draw all of the finder patterns required by the QR spec. Calculate the ratio - // of the number of rows/columns to the width and height - drawQrCodeFinders( - quietZonePx = columnWidth * QR_QUIET_ZONE_MODULES, - sideLength = size.width, - finderPatternSize = - Size( - width = columnWidth * FINDER_PATTERN_ROW_COUNT, - height = rowHeight * FINDER_PATTERN_ROW_COUNT, - ), - color = foregroundColor, - cornerRadius = radius, - ) - - // Draw data bits (encoded data part) - drawAllQrCodeDataBits( - quietZonePx = columnWidth * QR_QUIET_ZONE_MODULES, - bytes = qrCode.matrix, - size = - Size( - width = columnWidth, - height = rowHeight, - ), - color = foregroundColor, - ) + if (matrix != null) { + QrCanvas(matrix, foregroundColor) } } } -private typealias Coordinate = Pair +@Composable +private fun QrCanvas( + matrix: QrMatrix, + foregroundColor: Color, +) { + Canvas(modifier = Modifier.fillMaxSize()) { + // Calculate the height and width of each column/row + // Solve for the module size with the quiet zone measured in modules, so the whole code + // (zone included) is exactly as wide as the canvas. + val rowHeight = size.height / (matrix.height + QR_QUIET_ZONE_MODULES * 2f) + val columnWidth = size.width / (matrix.width + QR_QUIET_ZONE_MODULES * 2f) + // Scale the rounding with the module size. A fixed 20px radius is a gentle touch on + // a large code and a serious deformation on a small one: the finder patterns are what + // a decoder locates first, and rounding away a third of a module's worth of their + // corners is exactly the kind of damage that makes a code readable on screen and + // unreadable in a photo of that screen. + val radius = CornerRadius(min(columnWidth, rowHeight) * FINDER_CORNER_RADIUS_MODULES) -private fun createQrCode(contents: String): QRCode { - require(contents.isNotEmpty()) + // Draw all of the finder patterns required by the QR spec. Calculate the ratio + // of the number of rows/columns to the width and height + drawQrCodeFinders( + quietZonePx = columnWidth * QR_QUIET_ZONE_MODULES, + sideLength = size.width, + finderPatternSize = + Size( + width = columnWidth * FINDER_PATTERN_ROW_COUNT, + height = rowHeight * FINDER_PATTERN_ROW_COUNT, + ), + color = foregroundColor, + cornerRadius = radius, + ) - return Encoder.encode( - contents, - ErrorCorrectionLevel.Q, - mapOf( - EncodeHintType.CHARACTER_SET to "UTF-8", - EncodeHintType.MARGIN to QR_QUIET_ZONE_MODULES, - EncodeHintType.ERROR_CORRECTION to ErrorCorrectionLevel.Q, - ), - ) + // Draw data bits (encoded data part) + drawAllQrCodeDataBits( + quietZonePx = columnWidth * QR_QUIET_ZONE_MODULES, + bytes = matrix, + size = + Size( + width = columnWidth, + height = rowHeight, + ), + color = foregroundColor, + ) + } } +private typealias Coordinate = Pair + fun newPath(withPath: Path.() -> Unit) = Path().apply { fillType = PathFillType.EvenOdd @@ -157,7 +148,7 @@ fun newPath(withPath: Path.() -> Unit) = fun DrawScope.drawAllQrCodeDataBits( quietZonePx: Float, - bytes: ByteMatrix, + bytes: QrMatrix, size: Size, color: Color, ) { @@ -197,7 +188,7 @@ fun DrawScope.drawAllQrCodeDataBits( val newSize = Size(size.width + 0.5f, size.height + 0.5f) for (y in section.first.second until section.second.second) { for (x in section.first.first until section.second.first) { - if (bytes[x, y] == 1.toByte()) { + if (bytes[x, y]) { drawPath( color = color, path = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/QrCodeScanner.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrCodeScanner.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/QrCodeScanner.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrCodeScanner.kt index f935fd2575..ee525414ac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/QrCodeScanner.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrCodeScanner.kt @@ -18,16 +18,16 @@ * 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.qrcode +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.qrcode.ScannedPayload import com.vitorpamplona.amethyst.commons.qrcode.classifyScannedPayload +import com.vitorpamplona.amethyst.commons.ui.platform.QrCodeScannerDialog +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.ScanOutcome +import com.vitorpamplona.amethyst.commons.ui.uriToRoute import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.scanner.QrCodeScannerDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.scanner.ScanOutcome -import com.vitorpamplona.amethyst.ui.uriToRoute import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrMatrix.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrMatrix.kt new file mode 100644 index 0000000000..359f1d16c1 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrMatrix.kt @@ -0,0 +1,39 @@ +/* + * 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.commons.ui.screen.loggedIn.qrcode + +/** The dark/light modules of an encoded QR code, row-major, without its quiet zone. */ +class QrMatrix( + val width: Int, + val height: Int, + private val modules: BooleanArray, +) { + operator fun get( + x: Int, + y: Int, + ): Boolean = modules[y * width + x] +} + +/** + * Encodes [contents] (UTF-8, error correction Q) into its module grid, or null when this + * platform has no encoder yet or the payload does not fit a QR code. + */ +expect fun encodeQrMatrix(contents: String): QrMatrix? diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/ScanOutcome.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/ScanOutcome.kt new file mode 100644 index 0000000000..e3fcb38532 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/ScanOutcome.kt @@ -0,0 +1,30 @@ +/* + * 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.commons.ui.screen.loggedIn.qrcode + +/** What the caller did with a decoded payload — and therefore what the scanner does next. */ +enum class ScanOutcome { + /** The caller acted on it. Close the scanner. */ + Handled, + + /** Decoded fine, but this screen has nothing to do with it. Explain, and keep scanning. */ + NotSupported, +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/ShowQRScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/ShowQRScreen.kt index cfda159a4b..ecf96cdafb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/ShowQRScreen.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.qrcode +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -64,6 +64,8 @@ import com.vitorpamplona.amethyst.commons.ui.note.LoadUser import com.vitorpamplona.amethyst.commons.ui.note.ObserveAndDisplayNIP05 import com.vitorpamplona.amethyst.commons.ui.richtext.CreateTextWithEmoji import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.KeepScreenBrightAndAwake +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Font14SP import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt index 9a20f90a6d..99835b3daf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/redirect/LoadRedirectScreen.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.redirect +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.redirect import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -50,8 +50,8 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.withContext /** @@ -68,7 +68,7 @@ fun LoadRedirectScreen( eventId: String?, isPrivate: Boolean, accountViewModel: AccountViewModel, - nav: Nav, + nav: INav, ) { if (eventId == null) return diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/NewRelayNoteButton.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relay/NewRelayNoteButton.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/NewRelayNoteButton.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relay/NewRelayNoteButton.kt index 2597c7e193..6e1fe0d881 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/NewRelayNoteButton.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relay/NewRelayNoteButton.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.relay +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relay import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.FloatingActionButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/RelayFeedScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relay/RelayFeedScreen.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/RelayFeedScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relay/RelayFeedScreen.kt index 2ebfd0745b..f6f9c14465 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/RelayFeedScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relay/RelayFeedScreen.kt @@ -18,14 +18,12 @@ * 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.relay +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relay -import android.annotation.SuppressLint import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.profile.ui.FollowButton import com.vitorpamplona.amethyst.commons.profile.ui.UnfollowButton @@ -34,15 +32,16 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_follow import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_unfollow import com.vitorpamplona.amethyst.commons.resources.read_only_user +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.commons.ui.screen.RefresheableFeedView +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relay.dal.SingleRelayFeedViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relay.datasource.RelayFeedFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relay.dal.SingleRelayFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relay.datasource.RelayFeedFilterAssemblerSubscription import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer @@ -59,7 +58,7 @@ fun RelayFeedScreen( PrepareViewModelsRelayFeedScreen(tag, normalizedUrl, accountViewModel, nav) } -@SuppressLint("StateFlowValueCalledInComposition") +@Suppress("StateFlowValueCalledInComposition") @Composable fun PrepareViewModelsRelayFeedScreen( tag: Route.RelayFeed, @@ -68,7 +67,7 @@ fun PrepareViewModelsRelayFeedScreen( nav: INav, ) { val feedViewModel: SingleRelayFeedViewModel = - viewModel( + rememberViewModel( key = normalizedUrl.url + "SingleRelayFeedViewModel", factory = SingleRelayFeedViewModel.Factory( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/dal/RelayFeedFilter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relay/dal/RelayFeedFilter.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/dal/RelayFeedFilter.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relay/dal/RelayFeedFilter.kt index 197a39408e..658b7c0cd0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/dal/RelayFeedFilter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relay/dal/RelayFeedFilter.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.relay.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relay.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.feeds.sortedByDefaultFeedOrder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/dal/SingleRelayFeedViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relay/dal/SingleRelayFeedViewModel.kt similarity index 84% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/dal/SingleRelayFeedViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relay/dal/SingleRelayFeedViewModel.kt index f4ec796d47..ec1489e1ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/dal/SingleRelayFeedViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relay/dal/SingleRelayFeedViewModel.kt @@ -18,15 +18,17 @@ * 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.relay.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relay.dal import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlin.reflect.KClass @Stable class SingleRelayFeedViewModel( @@ -40,6 +42,9 @@ class SingleRelayFeedViewModel( val account: Account, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = SingleRelayFeedViewModel(relayUrl, account) as T + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = SingleRelayFeedViewModel(relayUrl, account) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/datasource/RelayFeedFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relay/datasource/RelayFeedFilterAssemblerSubscription.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/datasource/RelayFeedFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relay/datasource/RelayFeedFilterAssemblerSubscription.kt index c1bf03daa0..e866409b66 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relay/datasource/RelayFeedFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relay/datasource/RelayFeedFilterAssemblerSubscription.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.relay.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relay.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt index dffe0b19b5..4063628228 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.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.relayauth +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relayauth import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -62,7 +62,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -71,6 +70,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.nip11RelayInfo.loadRelayInfo +import com.vitorpamplona.amethyst.commons.relayClient.authCommand.compose.relayAuthPurposeLabelRes import com.vitorpamplona.amethyst.commons.relayauth.AuthPurposeKind import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthDecision import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthPermissionStore @@ -115,18 +115,18 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.note.timeAgo import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsDivider +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsSwitchTile import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.MediumRelayIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.RelayIconFilter import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.relayAuthPurposeLabelRes -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsDivider -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSwitchTile import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrlOrNull import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -199,7 +199,7 @@ fun RelayAuthSettingsScreen( val display = url.normalizeRelayUrlOrNull()?.displayUrl() ?: url val result = snackbarHostState.showSnackbar( - message = sessionForgottenLabel.format(display), + message = sessionForgottenLabel.replace("%1\$s", display), actionLabel = undoLabel, withDismissAction = true, ) @@ -209,7 +209,7 @@ fun RelayAuthSettingsScreen( // instead of leaving a tapped undo looking like it silently did nothing. if (result == SnackbarResult.ActionPerformed && !ledger.grantForSession(url)) { snackbarHostState.showSnackbar( - message = sessionUndoBlockedLabel.format(display), + message = sessionUndoBlockedLabel.replace("%1\$s", display), withDismissAction = true, ) } @@ -225,7 +225,7 @@ fun RelayAuthSettingsScreen( reloadKey++ val result = snackbarHostState.showSnackbar( - message = removedLabel.format(url.normalizeRelayUrlOrNull()?.displayUrl() ?: url), + message = removedLabel.replace("%1\$s", url.normalizeRelayUrlOrNull()?.displayUrl() ?: url), actionLabel = undoLabel, withDismissAction = true, ) @@ -650,7 +650,6 @@ private fun RecentLoginRow( nav: INav, onPromote: (RelayAuthDecision) -> Unit, ) { - val context = LocalContext.current RelayRowFrame( url = url, accountViewModel = accountViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/RelayInformationScreen.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/RelayInformationScreen.kt index e566d7e6de..156b7c0547 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/RelayInformationScreen.kt @@ -20,7 +20,7 @@ */ @file:Suppress("DEPRECATION") -package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -62,7 +62,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontFamily @@ -167,7 +166,9 @@ import com.vitorpamplona.amethyst.commons.ui.note.graspLink import com.vitorpamplona.amethyst.commons.ui.note.nipLink import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.BackButton import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.KindChip +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.datasource.RelayInfoNip66FilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.commons.ui.theme.Height25Modifier @@ -182,8 +183,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.warningColor import com.vitorpamplona.amethyst.commons.util.timeDiffAgoShortish import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.datasource.RelayInfoNip66FilterAssemblerSubscription import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.stats.ErrorDebugMessage import com.vitorpamplona.quartz.nip01Core.relay.client.stats.IRelayDebugMessage @@ -489,8 +488,6 @@ private fun FilterVisual( filter: Filter, index: Int, ) { - val context = LocalContext.current - OutlinedCard( modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), @@ -709,8 +706,6 @@ private fun OutboxEventsCard(eventIds: Set) { @Composable private fun RenderDebugMessage(msg: IRelayDebugMessage) { - val context = LocalContext.current - Column(Modifier.padding(horizontal = 12.dp)) { Row( modifier = Modifier.padding(vertical = 6.dp), @@ -1350,8 +1345,6 @@ private fun RelayMonitorReportCard( accountViewModel: AccountViewModel, nav: INav, ) { - val context = LocalContext.current - OutlinedCard( modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt similarity index 88% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt index b5fb025c3f..5964e1d7da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt @@ -18,11 +18,11 @@ * 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.relays.common +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common import androidx.compose.runtime.Immutable -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStat import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -41,6 +41,6 @@ fun relaySetupInfoBuilder( ): BasicRelaySetupInfo = BasicRelaySetupInfo( relay = normalized, - relayStat = Amethyst.instance.relayStats.get(normalized), + relayStat = LocalCache.appHost.relayStats?.get(normalized) ?: RelayStat(), forcesTor = forcesTor, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt index 995e4c5227..6aa2c29ac7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.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.relays.common +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.kt index c4b8f3205f..f2320212dd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.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.relays.common +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayStatusRow.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/RelayStatusRow.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayStatusRow.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/RelayStatusRow.kt index c239545f84..fc54797f18 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayStatusRow.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/RelayStatusRow.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.relays.common +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelaySuggestionState.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/RelaySuggestionState.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelaySuggestionState.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/RelaySuggestionState.kt index 21df27772c..d6879ce7e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelaySuggestionState.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/RelaySuggestionState.kt @@ -18,12 +18,13 @@ * 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.relays.common +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.debounce diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayUrlEditField.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/RelayUrlEditField.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayUrlEditField.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/RelayUrlEditField.kt index bc35ac9fcc..27d8cd16eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayUrlEditField.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/RelayUrlEditField.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.relays.common +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -46,12 +46,17 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.relays.nip11RelayInfo.Nip11CachedRetriever +import com.vitorpamplona.amethyst.commons.relays.nip11RelayInfo.Nip11Fetcher import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.add import com.vitorpamplona.amethyst.commons.resources.add_a_relay import com.vitorpamplona.amethyst.commons.resources.relay_url_not_valid import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.IRelaySuggestionState +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.RelaySuggestionState +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common.ShowRelaySuggestionList import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.commons.ui.theme.HalfHorzPadding @@ -120,7 +125,7 @@ fun RelayUrlEditFieldPreview() { RelayUrlEditField( onNewRelay = {}, relaySuggestions = relaySuggestions, - nip11CachedRetriever = Nip11CachedRetriever { TODO() }, + nip11CachedRetriever = Nip11CachedRetriever(Nip11Fetcher.Offline), modifier = Modifier, accountViewModel = accountViewModel, nav = EmptyNav(), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/ShowRelaySuggestionList.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/ShowRelaySuggestionList.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/ShowRelaySuggestionList.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/ShowRelaySuggestionList.kt index 81d2290cb7..6ead66081b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/ShowRelaySuggestionList.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/common/ShowRelaySuggestionList.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.relays.common +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.common import androidx.compose.foundation.layout.Column import androidx.compose.material3.HorizontalDivider diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/datasource/RelayInfoNip66FilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/datasource/RelayInfoNip66FilterAssemblerSubscription.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/datasource/RelayInfoNip66FilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/datasource/RelayInfoNip66FilterAssemblerSubscription.kt index 67a4f6789e..93f8b8ff7b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/datasource/RelayInfoNip66FilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/relays/datasource/RelayInfoNip66FilterAssemblerSubscription.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.relays.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/AllSettingsScreen.kt similarity index 91% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/AllSettingsScreen.kt index ec9e073d0d..171d3e2c51 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/AllSettingsScreen.kt @@ -18,9 +18,8 @@ * 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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -46,7 +45,6 @@ import androidx.compose.runtime.saveable.rememberSaveable 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.platform.LocalUriHandler import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -62,20 +60,27 @@ import com.vitorpamplona.amethyst.commons.resources.reset_marmot_confirm_title import com.vitorpamplona.amethyst.commons.resources.reset_marmot_failure import com.vitorpamplona.amethyst.commons.resources.reset_marmot_success import com.vitorpamplona.amethyst.commons.resources.settings_search_no_results +import com.vitorpamplona.amethyst.commons.ui.components.rememberLongNotice +import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SettingsSearchTopBar import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppPlatform +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsDivider +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsItem +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsSection import com.vitorpamplona.amethyst.commons.ui.settings.SettingsCategory import com.vitorpamplona.amethyst.commons.ui.settings.SettingsEntry import com.vitorpamplona.amethyst.commons.ui.settings.SettingsIcon import com.vitorpamplona.amethyst.commons.ui.settings.filterSettings +import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.stringRes import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.launch import org.jetbrains.compose.resources.StringResource @@ -95,7 +100,8 @@ fun AllSettingsScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val context = LocalContext.current + val shortNotice = rememberShortNotice() + val longNotice = rememberLongNotice() val uriHandler = LocalUriHandler.current val scope = rememberCoroutineScope() var showResetMarmotDialog by remember { mutableStateOf(false) } @@ -109,12 +115,14 @@ fun AllSettingsScreen( // an input actually changes — not on every keystroke. `onResetMarmot` reads the volatile // `isResettingMarmot` through `rememberUpdatedState` so the memoized closure never goes stale. val onResetMarmot by rememberUpdatedState(newValue = { if (!isResettingMarmot) showResetMarmotDialog = true }) + val appPlatform = LocalAppPlatform.current val catalog = - remember(hasPrivateKey, nav, uriHandler) { + remember(hasPrivateKey, nav, uriHandler, appPlatform) { buildSettingsCatalog( nav = nav, uriHandler = uriHandler, hasPrivateKey = hasPrivateKey, + legal = appPlatform.legalSettingsCategory(uriHandler), onResetMarmot = { onResetMarmot() }, ) } @@ -183,13 +191,13 @@ fun AllSettingsScreen( try { accountViewModel.resetMarmotState() launch(Dispatchers.Main) { - Toast.makeText(context, successMessage, Toast.LENGTH_SHORT).show() + shortNotice.show(successMessage) } } catch (e: Exception) { val failureMessage = loadStringRes(Res.string.reset_marmot_failure, e.message ?: "") launch(Dispatchers.Main) { - Toast.makeText(context, failureMessage, Toast.LENGTH_LONG).show() + longNotice.show(failureMessage) } } finally { isResettingMarmot = false diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AudioVisualizerSettingsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/AudioVisualizerSettingsScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AudioVisualizerSettingsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/AudioVisualizerSettingsScreen.kt index dc1a974b7e..fb72de7df4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AudioVisualizerSettingsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/AudioVisualizerSettingsScreen.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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -64,11 +64,11 @@ import com.vitorpamplona.amethyst.commons.resources.audio_visualizer_settings import com.vitorpamplona.amethyst.commons.resources.audio_visualizer_settings_description import com.vitorpamplona.amethyst.commons.resources.audio_visualizer_static import com.vitorpamplona.amethyst.commons.resources.audio_visualizer_waves +import com.vitorpamplona.amethyst.commons.ui.components.wavefront.FakeWaveformAnimation import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.service.playback.composable.wavefront.FakeWaveformAnimation import kotlinx.coroutines.flow.Flow @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/BlockedUsersScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/BlockedUsersScreen.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/BlockedUsersScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/BlockedUsersScreen.kt index d5c048507f..f6cb8a5d51 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/BlockedUsersScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/BlockedUsersScreen.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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold @@ -28,13 +28,13 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.blocked_users import com.vitorpamplona.amethyst.commons.resources.security_blocked_users_empty +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.dal.HiddenAccountsFeedViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal.HiddenAccountsFeedViewModel @Composable fun BlockedUsersScreen( @@ -42,7 +42,7 @@ fun BlockedUsersScreen( nav: INav, ) { val viewModel: HiddenAccountsFeedViewModel = - viewModel(factory = HiddenAccountsFeedViewModel.Factory(accountViewModel.account)) + rememberViewModel(factory = HiddenAccountsFeedViewModel.Factory(accountViewModel.account)) InvalidateOnBlockListChange(accountViewModel) { viewModel.invalidateData() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/BottomBarSettingsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/BottomBarSettingsScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/BottomBarSettingsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/BottomBarSettingsScreen.kt index 9b040cfbdd..e2945ef10b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/BottomBarSettingsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/BottomBarSettingsScreen.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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState @@ -67,7 +67,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppIcon import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -88,21 +87,22 @@ import com.vitorpamplona.amethyst.commons.resources.bottom_bar_settings_pinned import com.vitorpamplona.amethyst.commons.resources.bottom_bar_settings_pinned_empty import com.vitorpamplona.amethyst.commons.resources.bottom_bar_settings_remove import com.vitorpamplona.amethyst.commons.resources.bottom_bar_settings_reorder_hint +import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.BottomBarCategories +import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.GroupEntryAvatar +import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.GroupEntryDisplay +import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.NavBarCatalog +import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.NavBarCategory +import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.rememberFavoriteIconModel +import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.rememberGroupEntryDisplay import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices +import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size22Modifier import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.bottombars.BottomBarCategories -import com.vitorpamplona.amethyst.ui.navigation.bottombars.GroupEntryAvatar -import com.vitorpamplona.amethyst.ui.navigation.bottombars.GroupEntryDisplay -import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarCatalog -import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarCategory -import com.vitorpamplona.amethyst.ui.navigation.bottombars.rememberFavoriteIconModel -import com.vitorpamplona.amethyst.ui.navigation.bottombars.rememberGroupEntryDisplay -import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEntry import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.GroupTag import kotlinx.coroutines.flow.MutableStateFlow @@ -498,7 +498,7 @@ private fun PickerChildren( ) { when (item) { NavBarItem.BROWSER -> { - val favorites by Amethyst.instance.favoriteApps.favorites + val favorites by LocalAppServices.current.favoriteApps.favorites .collectAsStateWithLifecycle() if (favorites.isEmpty()) { EmptyChildHint(Res.string.bottom_bar_settings_no_favorites) @@ -813,7 +813,7 @@ private fun rememberPinnedVisual( PinnedVisual.Glyph(def?.icon ?: MaterialSymbols.Apps, def?.let { stringRes(it.labelRes) } ?: "") } is BottomBarEntry.Favorite -> { - val favorites by Amethyst.instance.favoriteApps.favorites + val favorites by LocalAppServices.current.favoriteApps.favorites .collectAsStateWithLifecycle() val app = favorites.firstOrNull { it.id == entry.favoriteId } if (app != null) PinnedVisual.Favorite(app) else PinnedVisual.Glyph(MaterialSymbols.Public, "") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/BottomBarSettingsState.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/BottomBarSettingsState.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/BottomBarSettingsState.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/BottomBarSettingsState.kt index 045c274f30..01542117a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/BottomBarSettingsState.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/BottomBarSettingsState.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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/DrawerSettingsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/DrawerSettingsScreen.kt similarity index 90% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/DrawerSettingsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/DrawerSettingsScreen.kt index 43afd795ad..ebacc2172e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/DrawerSettingsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/DrawerSettingsScreen.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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -58,18 +58,27 @@ import com.vitorpamplona.amethyst.commons.resources.drawer_settings_sections import com.vitorpamplona.amethyst.commons.resources.drawer_settings_show_all import com.vitorpamplona.amethyst.commons.resources.drawer_settings_title import com.vitorpamplona.amethyst.commons.resources.drawer_settings_visible +import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.NavBarCatalog +import com.vitorpamplona.amethyst.commons.ui.navigation.drawer.DrawerSection +import com.vitorpamplona.amethyst.commons.ui.navigation.drawer.DrawerSectionVisibility +import com.vitorpamplona.amethyst.commons.ui.navigation.drawer.DrawerSections import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.CatalogCard +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.CatalogRow +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.FlushPickerEditsOnExit +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.LeadingGlyph +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.PickerHeroCard +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.PickerSectionHeader +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.RestoreDefaultRow +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.TogglePill +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.rememberExpandedKeys import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarCatalog -import com.vitorpamplona.amethyst.ui.navigation.drawer.DrawerSection -import com.vitorpamplona.amethyst.ui.navigation.drawer.DrawerSectionVisibility -import com.vitorpamplona.amethyst.ui.navigation.drawer.DrawerSections @Composable @Preview(device = "spec:width=2100px,height=2340px,dpi=440") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/DrawerSettingsState.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/DrawerSettingsState.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/DrawerSettingsState.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/DrawerSettingsState.kt index fa5095f6f5..1d7676b88a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/DrawerSettingsState.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/DrawerSettingsState.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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue @@ -26,8 +26,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.vitorpamplona.amethyst.commons.model.navigation.DrawerItemVisibility import com.vitorpamplona.amethyst.commons.model.navigation.NavBarItem -import com.vitorpamplona.amethyst.ui.navigation.drawer.DrawerSection -import com.vitorpamplona.amethyst.ui.navigation.drawer.DrawerSectionVisibility +import com.vitorpamplona.amethyst.commons.ui.navigation.drawer.DrawerSection +import com.vitorpamplona.amethyst.commons.ui.navigation.drawer.DrawerSectionVisibility /** * State holder for the Side Menu settings screen: owns the set of switched-off drawer rows and the diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HiddenWordsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/HiddenWordsScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HiddenWordsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/HiddenWordsScreen.kt index d7db6d743b..b9cb2481cc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HiddenWordsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/HiddenWordsScreen.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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -59,7 +59,6 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.feeds.StringFeedState import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.hidden_words @@ -69,18 +68,19 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_ import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.resources.security_hidden_words_empty import com.vitorpamplona.amethyst.commons.ui.components.AddButton +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.commons.ui.insets.rememberSafeImeInsets import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.ShowUserButton +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.dal.HiddenWordsFeedViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.HorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.StdPadding import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal.HiddenWordsFeedViewModel @Composable fun HiddenWordsScreen( @@ -88,7 +88,7 @@ fun HiddenWordsScreen( nav: INav, ) { val viewModel: HiddenWordsFeedViewModel = - viewModel(factory = HiddenWordsFeedViewModel.Factory(accountViewModel.account)) + rememberViewModel(factory = HiddenWordsFeedViewModel.Factory(accountViewModel.account)) InvalidateOnBlockListChange(accountViewModel) { viewModel.invalidateData() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.kt index 1a689f57c3..cacde05552 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/MutedThreadsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/MutedThreadsScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/MutedThreadsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/MutedThreadsScreen.kt index 75bf656450..63e31207cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/MutedThreadsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/MutedThreadsScreen.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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -43,7 +43,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.resources.Res @@ -51,11 +50,13 @@ import com.vitorpamplona.amethyst.commons.resources.action_unmute import com.vitorpamplona.amethyst.commons.resources.settings_muted_threads_empty import com.vitorpamplona.amethyst.commons.resources.settings_muted_threads_title import com.vitorpamplona.amethyst.commons.resources.settings_muted_threads_unknown +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.dal.MutedThreadsFeedViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.commons.ui.theme.ButtonPadding @@ -64,7 +65,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp import com.vitorpamplona.amethyst.commons.ui.theme.Size15dp import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal.MutedThreadsFeedViewModel @Composable fun MutedThreadsScreen( @@ -72,7 +72,7 @@ fun MutedThreadsScreen( nav: INav, ) { val viewModel: MutedThreadsFeedViewModel = - viewModel(factory = MutedThreadsFeedViewModel.Factory(accountViewModel.account)) + rememberViewModel(factory = MutedThreadsFeedViewModel.Factory(accountViewModel.account)) InvalidateOnBlockListChange(accountViewModel) { viewModel.invalidateData() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NavPickerUi.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NavPickerUi.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NavPickerUi.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NavPickerUi.kt index 1985b6b1cb..1775e1bfae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NavPickerUi.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/NavPickerUi.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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt index f3f4610603..6e93d99d94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/ProfileUiSettingsScreen.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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt index 8e1bfb4217..680649a54b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SecurityFiltersScreen.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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityListsCommon.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SecurityListsCommon.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityListsCommon.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SecurityListsCommon.kt index 495be15db8..11a618e52b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityListsCommon.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SecurityListsCommon.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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.ShowUserButton import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.commons.ui.note.buttons.CloseButton +import com.vitorpamplona.amethyst.commons.ui.screen.UserFeedViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding @@ -77,7 +78,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp import com.vitorpamplona.amethyst.commons.ui.theme.Size15dp import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel import org.jetbrains.compose.resources.StringResource /** @@ -225,7 +225,7 @@ internal fun SelectableUserList( } @Composable -internal fun EmptyState(message: StringResource) { +fun EmptyState(message: StringResource) { Column( modifier = Modifier.fillMaxSize().padding(32.dp), horizontalAlignment = Alignment.CenterHorizontally, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt index ff9348aea5..0bf5bee248 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt @@ -18,10 +18,9 @@ * 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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.ui.platform.UriHandler -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.navigation.Route @@ -54,6 +53,7 @@ import com.vitorpamplona.amethyst.commons.resources.event_sync_title import com.vitorpamplona.amethyst.commons.resources.favorite_dvms_search_keywords import com.vitorpamplona.amethyst.commons.resources.favorite_dvms_title import com.vitorpamplona.amethyst.commons.resources.home_tabs_search_keywords +import com.vitorpamplona.amethyst.commons.resources.ic_tor import com.vitorpamplona.amethyst.commons.resources.import_follows_search_keywords import com.vitorpamplona.amethyst.commons.resources.media_servers import com.vitorpamplona.amethyst.commons.resources.media_servers_search_keywords @@ -85,6 +85,7 @@ import com.vitorpamplona.amethyst.commons.resources.relay_auth_search_keywords import com.vitorpamplona.amethyst.commons.resources.relay_auth_settings_title import com.vitorpamplona.amethyst.commons.resources.relay_setup import com.vitorpamplona.amethyst.commons.resources.relay_setup_search_keywords +import com.vitorpamplona.amethyst.commons.resources.relays import com.vitorpamplona.amethyst.commons.resources.request_to_vanish import com.vitorpamplona.amethyst.commons.resources.request_to_vanish_search_keywords import com.vitorpamplona.amethyst.commons.resources.reset_marmot_search_keywords @@ -120,6 +121,7 @@ fun buildSettingsCatalog( nav: INav, uriHandler: UriHandler, hasPrivateKey: Boolean, + legal: SettingsCategory?, onResetMarmot: () -> Unit, ): List { // Most rows are a symbol icon + a keyword blob that navigates to a route. This local @@ -143,7 +145,7 @@ fun buildSettingsCatalog( listOf( SettingsEntry( titleRes = Res.string.relay_setup, - icon = SettingsIcon.Painter(R.drawable.relays, 4), + icon = SettingsIcon.Painter(Res.drawable.relays, 4), keywordsRes = Res.string.relay_setup_search_keywords, ) { nav.nav(Route.EditRelays) }, symEntry(Res.string.event_sync_title, MaterialSymbols.Sync, Res.string.event_sync_search_keywords, Route.EventSync), @@ -185,7 +187,7 @@ fun buildSettingsCatalog( listOf( SettingsEntry( titleRes = Res.string.privacy_options, - icon = SettingsIcon.Painter(R.drawable.ic_tor, 1), + icon = SettingsIcon.Painter(Res.drawable.ic_tor, 1), keywordsRes = Res.string.privacy_options_search_keywords, ) { nav.nav(Route.PrivacyOptions) }, symEntry(Res.string.ui_preferences, MaterialSymbols.Settings, Res.string.ui_preferences_search_keywords, Route.Settings), @@ -245,5 +247,5 @@ fun buildSettingsCatalog( }, ) - return listOfNotNull(account, app, legalSettingsCategory(uriHandler), danger) + return listOfNotNull(account, app, legal, danger) } diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SettingsRows.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SettingsRows.kt new file mode 100644 index 0000000000..e6e6a3a8c5 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SettingsRows.kt @@ -0,0 +1,183 @@ +/* + * 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.commons.ui.screen.loggedIn.settings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol +import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner +import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer +import com.vitorpamplona.amethyst.commons.ui.stringRes +import kotlinx.collections.immutable.ImmutableList +import org.jetbrains.compose.resources.StringResource + +/** + * A [SettingsBlockTile] whose control is a full-width [SingleChoiceSegmentedButtonRow]. + * This is the in-screen replacement for the old dropdown ([TextSpinner]) rows: every + * option is visible and one tap away. Best for 2–4 mutually-exclusive options. + * + * [optionTextStyle] lets each option render its own label preview — e.g. the font tiles + * draw each label in the very typeface / size it selects, so the row demonstrates the + * choices instead of only naming them. + */ +@Composable +fun SegmentedChoiceTile( + icon: MaterialSymbol, + title: StringResource, + description: StringResource, + options: List, + labelRes: (T) -> StringResource, + selected: T, + onSelect: (T) -> Unit, + optionTextStyle: (@Composable (T) -> TextStyle)? = null, +) { + SettingsBlockTile( + icon = icon, + title = stringRes(title), + description = stringRes(description), + ) { + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + options.forEachIndexed { index, option -> + SegmentedButton( + selected = option == selected, + onClick = { onSelect(option) }, + shape = SegmentedButtonDefaults.itemShape(index = index, count = options.size), + // Drop the default check icon: the fill already signals selection, and the icon + // steals ~24dp that the label needs in a 3–4-up row. + icon = {}, + ) { + Text( + text = stringRes(labelRes(option)), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = optionTextStyle?.invoke(option) ?: LocalTextStyle.current, + ) + } + } + } + } +} + +@Composable +fun SettingsRow( + name: StringResource, + description: StringResource, + selectedItems: ImmutableList, + selectedIndex: Int, + onSelect: (Int) -> Unit, +) { + SettingsRow(name, description) { + TextSpinner( + label = "", + placeholder = selectedItems[selectedIndex].title, + options = selectedItems, + onSelect = onSelect, + modifier = Modifier.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)), + ) + } +} + +@Composable +fun SettingsRow( + name: StringResource, + description: StringResource, + modifier: Modifier = Modifier.fillMaxWidth(), + content: @Composable () -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(5.dp), + ) { + Column( + modifier = Modifier.weight(2.0f), + verticalArrangement = Arrangement.spacedBy(3.dp), + ) { + Text( + text = stringRes(name), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = stringRes(description), + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + + Column( + Modifier.weight(1f), + horizontalAlignment = Alignment.End, + ) { + content() + } + } +} + +@Composable +fun SettingsRow( + name: StringResource, + description: StringResource, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp), + ) { + Column( + modifier = Modifier.weight(2.0f), + verticalArrangement = Arrangement.spacedBy(3.dp), + ) { + Text( + text = stringRes(name), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = stringRes(description), + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsSectionCard.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SettingsSectionCard.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsSectionCard.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SettingsSectionCard.kt index 92d2e53f19..648a48ba52 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsSectionCard.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SettingsSectionCard.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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -51,13 +51,14 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.ui.painterRes +import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.StringResource import androidx.compose.material3.Icon as Material3Icon @Composable -internal fun SettingsSection( +fun SettingsSection( title: StringResource, isDanger: Boolean = false, content: @Composable ColumnScope.() -> Unit, @@ -94,7 +95,7 @@ internal fun SettingsSection( } @Composable -internal fun SettingsDivider() { +fun SettingsDivider() { HorizontalDivider( modifier = Modifier.padding(start = 68.dp), thickness = 0.5.dp, @@ -104,7 +105,7 @@ internal fun SettingsDivider() { /** Navigation row: icon + title + optional [trailing] (count badge, etc.) + chevron. */ @Composable -internal fun SettingsItem( +fun SettingsItem( title: StringResource, icon: MaterialSymbol, isDanger: Boolean = false, @@ -128,9 +129,9 @@ internal fun SettingsItem( /** Navigation row variant taking a drawable painter as the leading icon. */ @Composable -internal fun SettingsItem( +fun SettingsItem( title: StringResource, - iconPainter: Int, + iconPainter: DrawableResource, iconPainterRef: Int, isDanger: Boolean = false, trailing: @Composable () -> Unit = {}, @@ -213,7 +214,7 @@ private fun SettingsItemRow( * tapping anywhere should toggle the state. */ @Composable -internal fun SettingsControlRow( +fun SettingsControlRow( icon: MaterialSymbol, title: String, description: String, @@ -255,7 +256,7 @@ internal fun SettingsControlRow( /** A [SettingsControlRow] whose trailing control is a [Switch]; tapping anywhere toggles. */ @Composable -internal fun SettingsSwitchTile( +fun SettingsSwitchTile( icon: MaterialSymbol, title: StringResource, description: StringResource, @@ -279,7 +280,7 @@ internal fun SettingsSwitchTile( * (used to lock the last remaining item in a "keep at least one on" group). */ @Composable -internal fun SettingsSwitchTile( +fun SettingsSwitchTile( icon: MaterialSymbol, title: StringResource, checked: Boolean, @@ -322,7 +323,7 @@ internal fun SettingsSwitchTile( * that only matters when the parent toggle is on). Text dims when [enabled] is false. */ @Composable -internal fun SettingsSubControlRow( +fun SettingsSubControlRow( title: String, description: String, enabled: Boolean = true, @@ -357,7 +358,7 @@ internal fun SettingsSubControlRow( * SegmentedButtonRow). Use when the control would crowd a trailing slot. */ @Composable -internal fun SettingsBlockTile( +fun SettingsBlockTile( icon: MaterialSymbol, title: String, description: String, @@ -428,7 +429,7 @@ internal fun SettingsCountBadge(count: Int) { * `null` to always show the number. */ @Composable -internal fun SettingsStepper( +fun SettingsStepper( value: Int, min: Int, max: Int, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SpammingUsersScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SpammingUsersScreen.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SpammingUsersScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SpammingUsersScreen.kt index 42ff801b75..75d55e3df7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SpammingUsersScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/SpammingUsersScreen.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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold @@ -28,13 +28,13 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.security_spamming_users_empty import com.vitorpamplona.amethyst.commons.resources.spamming_users +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.dal.SpammerAccountsFeedViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal.SpammerAccountsFeedViewModel @Composable fun SpammingUsersScreen( @@ -42,7 +42,7 @@ fun SpammingUsersScreen( nav: INav, ) { val viewModel: SpammerAccountsFeedViewModel = - viewModel(factory = SpammerAccountsFeedViewModel.Factory(accountViewModel.account)) + rememberViewModel(factory = SpammerAccountsFeedViewModel.Factory(accountViewModel.account)) InvalidateOnBlockListChange(accountViewModel) { viewModel.invalidateData() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/StringFeedViewModel.kt similarity index 91% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/StringFeedViewModel.kt index b4a28274ee..627284eb44 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/StringFeedViewModel.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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable @@ -31,11 +31,11 @@ import com.vitorpamplona.amethyst.commons.feeds.StringFeedState import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.service.BundledUpdate import com.vitorpamplona.amethyst.commons.util.equalImmutableLists -import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.utils.Log import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch @@ -55,8 +55,6 @@ open class StringFeedViewModel( } private fun refreshSuspended() { - checkNotInMainThread() - try { isRefreshing.value = true @@ -99,23 +97,23 @@ open class StringFeedViewModel( } init { - Log.d("Init", this.javaClass.simpleName) + Log.d("Init") { "${this::class.simpleName}" } viewModelScope.launch(Dispatchers.IO) { LocalCache.live.newEventBundles.collect { newNotes -> - Log.d("Rendering Metrics") { "Update feeds: ${this@StringFeedViewModel.javaClass.simpleName} with ${newNotes.size}" } + Log.d("Rendering Metrics") { "Update feeds: ${this@StringFeedViewModel::class.simpleName} with ${newNotes.size}" } invalidateData() } } viewModelScope.launch(Dispatchers.IO) { LocalCache.live.deletedEventBundles.collect { newNotes -> - Log.d("Rendering Metrics") { "Delete feeds: ${this@StringFeedViewModel.javaClass.simpleName} with ${newNotes.size}" } + Log.d("Rendering Metrics") { "Delete feeds: ${this@StringFeedViewModel::class.simpleName} with ${newNotes.size}" } invalidateData() } } } override fun onCleared() { - Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" } + Log.d("Init") { "OnCleared: ${this::class.simpleName}" } bundler.cancel() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/VideoPlayerSettingsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/VideoPlayerSettingsScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/VideoPlayerSettingsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/VideoPlayerSettingsScreen.kt index 918d6e76bc..877ff37712 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/VideoPlayerSettingsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/VideoPlayerSettingsScreen.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.settings +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.ScrollState @@ -58,7 +58,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.VideoButtonLocation @@ -89,6 +88,7 @@ import com.vitorpamplona.amethyst.commons.resources.video_player_settings_reorde import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppPlatform import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow @@ -125,8 +125,9 @@ fun VideoPlayerSettingsScreen( @Composable fun VideoPlayerSettingsContent(accountViewModel: AccountViewModel) { val buttonItems by accountViewModel.videoPlayerButtonItemsFlow().collectAsStateWithLifecycle() + val isCastingAvailable = LocalAppPlatform.current.isCastingAvailable val displayedItems = - if (BuildConfig.IS_CASTING_AVAILABLE) { + if (isCastingAvailable) { buttonItems } else { buttonItems.filter { it.action != VideoPlayerAction.Cast } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt index f8659c8274..3bc74a3606 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.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.settings.cordn +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -115,13 +115,13 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackBu import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CopyableKeyRow +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsSection import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.CoordinatorIdentityRow import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.coordinatorDisplayName import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CopyableKeyRow -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSection import com.vitorpamplona.quartz.cordn.spec00Coordinator.CoordinatorServerInfo import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnHubScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/cordn/CordnHubScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnHubScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/cordn/CordnHubScreen.kt index 0c820afc23..36e1311077 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnHubScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/cordn/CordnHubScreen.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.settings.cordn +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -55,13 +55,13 @@ import com.vitorpamplona.amethyst.commons.resources.cordn_hub_title import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsControlRow +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsDivider +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsSection import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsControlRow -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsDivider -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSection import org.jetbrains.compose.resources.StringResource /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnKeyPackagesScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/cordn/CordnKeyPackagesScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnKeyPackagesScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/cordn/CordnKeyPackagesScreen.kt index 4ba0e5df10..5280a180ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnKeyPackagesScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/cordn/CordnKeyPackagesScreen.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.settings.cordn +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -76,10 +76,10 @@ import com.vitorpamplona.amethyst.commons.ui.components.EmptyState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsSection import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.CoordinatorIdentityRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSection import kotlinx.coroutines.launch /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnLinkScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/cordn/CordnLinkScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnLinkScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/cordn/CordnLinkScreen.kt index 41f3e13cd8..f02e04858f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnLinkScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/cordn/CordnLinkScreen.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.settings.cordn +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -69,11 +69,11 @@ import com.vitorpamplona.amethyst.commons.resources.cordn_link_section_input import com.vitorpamplona.amethyst.commons.resources.cordn_link_title import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.SettingsSection import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.cordn.CoordinatorIdentityRow import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSection import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.kt index 7cc9eb83c4..c0ba15dee9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.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.settings.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.dal import com.vitorpamplona.amethyst.commons.feeds.FeedFilter import com.vitorpamplona.amethyst.commons.model.Account diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedViewModel.kt similarity index 70% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedViewModel.kt index f572ce2a48..8b92bc28de 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedViewModel.kt @@ -18,12 +18,14 @@ * 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.settings.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account -import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.UserFeedViewModel +import kotlin.reflect.KClass class HiddenAccountsFeedViewModel( val account: Account, @@ -31,12 +33,14 @@ class HiddenAccountsFeedViewModel( class Factory( val account: Account, ) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(HiddenAccountsFeedViewModel::class.java)) { - @Suppress("UNCHECKED_CAST") - return HiddenAccountsFeedViewModel(account) as T - } - throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T { + // Like the sibling feed factories: a caller may ask for a supertype + // (`UserFeedViewModel`), which a class-equality check would reject. + @Suppress("UNCHECKED_CAST") + return HiddenAccountsFeedViewModel(account) as T } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenWordsFeedFilter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/HiddenWordsFeedFilter.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenWordsFeedFilter.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/HiddenWordsFeedFilter.kt index 9c3b992610..7db8a143cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenWordsFeedFilter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/HiddenWordsFeedFilter.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.settings.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.dal import com.vitorpamplona.amethyst.commons.feeds.FeedFilter import com.vitorpamplona.amethyst.commons.model.Account diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenWordsFeedViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/HiddenWordsFeedViewModel.kt similarity index 78% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenWordsFeedViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/HiddenWordsFeedViewModel.kt index 1c752650c4..c450216e15 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenWordsFeedViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/HiddenWordsFeedViewModel.kt @@ -18,12 +18,14 @@ * 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.settings.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.StringFeedViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.StringFeedViewModel +import kotlin.reflect.KClass class HiddenWordsFeedViewModel( val account: Account, @@ -34,6 +36,9 @@ class HiddenWordsFeedViewModel( val account: Account, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = HiddenWordsFeedViewModel(account) as T + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = HiddenWordsFeedViewModel(account) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/MutedThreadsFeedFilter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/MutedThreadsFeedFilter.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/MutedThreadsFeedFilter.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/MutedThreadsFeedFilter.kt index 2d85c61b14..d58cdd8d97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/MutedThreadsFeedFilter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/MutedThreadsFeedFilter.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.settings.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.dal import com.vitorpamplona.amethyst.commons.feeds.FeedFilter import com.vitorpamplona.amethyst.commons.model.Account diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/MutedThreadsFeedViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/MutedThreadsFeedViewModel.kt similarity index 83% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/MutedThreadsFeedViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/MutedThreadsFeedViewModel.kt index 4f415fa9f7..544f320996 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/MutedThreadsFeedViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/MutedThreadsFeedViewModel.kt @@ -18,13 +18,15 @@ * 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.settings.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.dal import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.ui.screen.AndroidFeedViewModel +import kotlin.reflect.KClass @Stable class MutedThreadsFeedViewModel( @@ -34,6 +36,9 @@ class MutedThreadsFeedViewModel( val account: Account, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = MutedThreadsFeedViewModel(account) as T + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = MutedThreadsFeedViewModel(account) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt index b125a22065..1bed285d80 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.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.settings.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.dal import com.vitorpamplona.amethyst.commons.feeds.FeedFilter import com.vitorpamplona.amethyst.commons.model.Account diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedViewModel.kt similarity index 79% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedViewModel.kt index 353bb2bd9c..e27ceef912 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedViewModel.kt @@ -18,12 +18,14 @@ * 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.settings.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account -import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.UserFeedViewModel +import kotlin.reflect.KClass class SpammerAccountsFeedViewModel( val account: Account, @@ -32,6 +34,9 @@ class SpammerAccountsFeedViewModel( val account: Account, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = SpammerAccountsFeedViewModel(account) as T + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = SpammerAccountsFeedViewModel(account) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt index 37040337e7..772d4c261f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt @@ -18,10 +18,8 @@ * 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.settings.nip46 +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.nip46 -import android.content.Context -import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat @@ -73,7 +71,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboard -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -119,16 +116,18 @@ import com.vitorpamplona.amethyst.commons.resources.nip46_signer_scan_connect import com.vitorpamplona.amethyst.commons.resources.nip46_signer_status_no_relays import com.vitorpamplona.amethyst.commons.resources.nip46_signer_title import com.vitorpamplona.amethyst.commons.resources.nip46_signer_turn_on +import com.vitorpamplona.amethyst.commons.ui.components.rememberLongNotice +import com.vitorpamplona.amethyst.commons.ui.components.rememberShortNotice import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar import com.vitorpamplona.amethyst.commons.ui.pluralStringRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner import kotlinx.coroutines.launch private val LiveGreen = Color(0xFF3DDC84) @@ -142,7 +141,8 @@ fun Nip46SignerScreen( val account = accountViewModel.account val signer = account.nip46Signer val scope = rememberCoroutineScope() - val context = LocalContext.current + val shortNotice = rememberShortNotice() + val longNotice = rememberLongNotice() val clipboard = LocalClipboard.current val copiedStr = stringRes(Res.string.nip46_signer_copied) val regeneratedStr = stringRes(Res.string.nip46_signer_regenerated) @@ -175,7 +175,7 @@ fun Nip46SignerScreen( fun onConnect(uri: String) { scope.launch { val result = signer.connectViaNostrConnect(uri.trim()) - Toast.makeText(context, describe(context, result), Toast.LENGTH_LONG).show() + longNotice.show(describe(result)) refreshKey++ } } @@ -231,7 +231,7 @@ fun Nip46SignerScreen( uri = uri, onCopy = { scope.launch { clipboard.setText(uri) } - Toast.makeText(context, copiedStr, Toast.LENGTH_SHORT).show() + shortNotice.show(copiedStr) }, onRegenerate = { confirmRotate = true }, ) @@ -273,7 +273,7 @@ fun Nip46SignerScreen( onConfirm = { confirmRotate = false signer.rotateAddress() - Toast.makeText(context, regeneratedStr, Toast.LENGTH_SHORT).show() + shortNotice.show(regeneratedStr) }, onDismiss = { confirmRotate = false }, ) @@ -625,10 +625,7 @@ private fun ReadOnlyNotice() { } } -private suspend fun describe( - context: Context, - result: Nip46SignerState.ConnectResult, -): String = +private suspend fun describe(result: Nip46SignerState.ConnectResult): String = when (result) { is Nip46SignerState.ConnectResult.Connected -> result.name?.let { loadStringRes(Res.string.nip46_signer_connected_named, it) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt index b4a2faa10d..ce0470e02c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/softwareapps/SoftwareAppDetailScreen.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.softwareapps +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.softwareapps import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -52,7 +52,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note @@ -68,6 +67,7 @@ import com.vitorpamplona.amethyst.commons.resources.nip82_section_latest_release import com.vitorpamplona.amethyst.commons.resources.nip82_section_links import com.vitorpamplona.amethyst.commons.resources.nip82_section_platforms import com.vitorpamplona.amethyst.commons.resources.nip82_section_topics +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding @@ -75,6 +75,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose +import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow import com.vitorpamplona.amethyst.commons.ui.note.types.AppAuthorLine import com.vitorpamplona.amethyst.commons.ui.note.types.AppIcon import com.vitorpamplona.amethyst.commons.ui.note.types.AppLinksColumn @@ -87,6 +88,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.types.TopicChipFlow import com.vitorpamplona.amethyst.commons.ui.note.types.VersionChip import com.vitorpamplona.amethyst.commons.ui.note.types.findAllNip82Releases import com.vitorpamplona.amethyst.commons.ui.note.types.findLatestNip82Release +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.threadview.dal.ThreadFeedViewModel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.threadview.datasources.ThreadFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness @@ -99,8 +101,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder import com.vitorpamplona.amethyst.commons.ui.thread.drawReplyLevel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.ReactionsRow -import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.ThreadFeedViewModel import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent import com.vitorpamplona.quartz.nip01Core.core.Address @@ -133,7 +133,7 @@ private fun SoftwareAppDetailScreenContent( // resolves that to the application note and then walks replies. val addressTag = note.idHex val threadViewModel: ThreadFeedViewModel = - viewModel( + rememberViewModel( key = addressTag + "SoftwareAppDetailThread", factory = ThreadFeedViewModel.Factory(accountViewModel.account, addressTag), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/threadview/dal/ThreadFeedViewModel.kt similarity index 76% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/threadview/dal/ThreadFeedViewModel.kt index ea83c6277f..806a25c7a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/threadview/dal/ThreadFeedViewModel.kt @@ -18,23 +18,29 @@ * 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.threadview.dal +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.threadview.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.viewmodels.thread.LevelFeedViewModel import com.vitorpamplona.amethyst.commons.viewmodels.thread.ThreadFeedFilter +import kotlin.reflect.KClass class ThreadFeedViewModel( account: Account, noteId: String, -) : com.vitorpamplona.amethyst.commons.viewmodels.thread.LevelFeedViewModel(ThreadFeedFilter(account, noteId, LocalCache), LocalCache) { +) : LevelFeedViewModel(ThreadFeedFilter(account, noteId, LocalCache), LocalCache) { class Factory( val account: Account, val noteId: String, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = ThreadFeedViewModel(account, noteId) as T + override fun create( + modelClass: KClass, + extras: CreationExtras, + ): T = ThreadFeedViewModel(account, noteId) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/AddClinkDebitWalletScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/AddClinkDebitWalletScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/AddClinkDebitWalletScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/AddClinkDebitWalletScreen.kt index c01f2fd456..dfa1d340da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/AddClinkDebitWalletScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/AddClinkDebitWalletScreen.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.wallet +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -49,7 +49,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res @@ -63,14 +62,15 @@ import com.vitorpamplona.amethyst.commons.resources.wallet_name import com.vitorpamplona.amethyst.commons.resources.wallet_name_hint import com.vitorpamplona.amethyst.commons.resources.wallet_paste_ndebit import com.vitorpamplona.amethyst.commons.resources.wallet_save +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.components.util.getText import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size24Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner import kotlinx.coroutines.launch /** @@ -85,7 +85,7 @@ fun AddClinkDebitWalletScreen( nav: INav, ndebit: String? = null, ) { - val walletViewModel: WalletViewModel = viewModel() + val walletViewModel: WalletViewModel = rememberViewModel { WalletViewModel() } walletViewModel.init(accountViewModel) var walletName by remember { mutableStateOf("") } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/AddNwcWalletScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/AddNwcWalletScreen.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/AddNwcWalletScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/AddNwcWalletScreen.kt index cdb05c7e33..d5f64f549a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/AddNwcWalletScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/AddNwcWalletScreen.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.wallet +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -51,7 +51,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res @@ -67,16 +66,17 @@ import com.vitorpamplona.amethyst.commons.resources.wallet_name import com.vitorpamplona.amethyst.commons.resources.wallet_name_hint import com.vitorpamplona.amethyst.commons.resources.wallet_paste_uri import com.vitorpamplona.amethyst.commons.resources.wallet_save +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.components.util.getText import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.Size24Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner import com.vitorpamplona.quartz.nip47WalletConnect.Nip47DeepLink import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import kotlinx.coroutines.CancellationException @@ -89,7 +89,7 @@ fun AddNwcWalletScreen( nav: INav, nip47Uri: String? = null, ) { - val walletViewModel: WalletViewModel = viewModel() + val walletViewModel: WalletViewModel = rememberViewModel { WalletViewModel() } walletViewModel.init(accountViewModel) var walletName by remember { mutableStateOf("") } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/CashuWalletScreen.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/CashuWalletScreen.kt index c2394ff37e..5a644873c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/CashuWalletScreen.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.wallet +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -77,7 +77,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.hashtags.Cashu import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -156,12 +155,15 @@ import com.vitorpamplona.amethyst.commons.resources.paste_from_clipboard import com.vitorpamplona.amethyst.commons.resources.topup_mint_action import com.vitorpamplona.amethyst.commons.resources.wallet_add_cashu_title import com.vitorpamplona.amethyst.commons.resources.wallet_sats +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.components.util.getText import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.DateTimeStyle import com.vitorpamplona.amethyst.commons.ui.note.LoadUser import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.commons.ui.note.formatDateTime import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.CashuMeltFlowState import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.CashuMintFlowState @@ -170,6 +172,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.CashuRedeemF import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.CashuSendTokenFlowState import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.CashuWalletViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.util.formatGrouped import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip60Cashu.history.CashuSpendingHistoryEvent @@ -180,9 +183,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch -import java.text.DateFormat -import java.text.NumberFormat -import java.util.Date import androidx.compose.material3.Icon as Material3Icon @OptIn(ExperimentalMaterial3Api::class) @@ -191,7 +191,7 @@ fun CashuWalletScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val viewModel: CashuWalletViewModel = viewModel() + val viewModel: CashuWalletViewModel = rememberViewModel { CashuWalletViewModel() } // Synchronous init so state-flow getters don't hit a null `account` on // the first composition pass. init() is idempotent — just sets refs. viewModel.init(accountViewModel) @@ -882,7 +882,7 @@ private fun EvacuateMintDialog( private fun BalanceCard(balanceSats: Long) { val formatted = remember(balanceSats) { - NumberFormat.getIntegerInstance().format(balanceSats) + formatGrouped(balanceSats) } Card( modifier = Modifier.fillMaxWidth(), @@ -990,7 +990,7 @@ private fun MintRow( ) { val formattedBalance = remember(balanceSats) { - NumberFormat.getIntegerInstance().format(balanceSats) + formatGrouped(balanceSats) } Card( modifier = Modifier.fillMaxWidth(), @@ -1151,9 +1151,7 @@ private fun HistoryRow( } Text( text = - DateFormat - .getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT) - .format(Date(entry.createdAt * 1000)), + formatDateTime(entry.createdAt * 1000, DateTimeStyle.MEDIUM, DateTimeStyle.SHORT), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -1163,7 +1161,7 @@ private fun HistoryRow( val amount = decoded?.amount if (amount != null) { Text( - text = (if (isIncoming) "+" else "−") + NumberFormat.getIntegerInstance().format(amount), + text = (if (isIncoming) "+" else "−") + formatGrouped(amount), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.SemiBold, color = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainSection.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/OnchainSection.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainSection.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/OnchainSection.kt index e331aa82c6..ac265072da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainSection.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/OnchainSection.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.wallet +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background @@ -80,9 +80,9 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.sats import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.bitcoinColor +import com.vitorpamplona.amethyst.commons.util.formatGrouped import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import kotlinx.coroutines.launch -import java.text.NumberFormat /** * "Bitcoin" card on the wallet screen, shown above the lightning NWC wallet @@ -277,7 +277,7 @@ private fun BalanceBlock( OnchainBalanceStatus.READY -> { val formatted = remember(sats) { - NumberFormat.getIntegerInstance().format(sats ?: 0L) + formatGrouped(sats ?: 0L) } Text( text = formatted, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainTransactionsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/OnchainTransactionsScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainTransactionsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/OnchainTransactionsScreen.kt index 3b7d3f2b63..5887f121c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainTransactionsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/OnchainTransactionsScreen.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.wallet +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -51,14 +51,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.navigation.Route @@ -77,16 +75,18 @@ import com.vitorpamplona.amethyst.commons.resources.wallet_onchain_pending import com.vitorpamplona.amethyst.commons.resources.wallet_onchain_transactions import com.vitorpamplona.amethyst.commons.resources.wallet_outgoing import com.vitorpamplona.amethyst.commons.resources.wallet_refresh +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.LoadUser import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.commons.ui.note.rememberMonthDayTimeFormatter +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.TransactionFilter +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.datasource.OnchainZapsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.bitcoinColor +import com.vitorpamplona.amethyst.commons.util.formatGrouped import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.formatMonthDayTime -import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.datasource.OnchainZapsFilterAssemblerSubscription -import java.text.NumberFormat import kotlin.math.absoluteValue @OptIn(ExperimentalMaterial3Api::class) @@ -95,7 +95,7 @@ fun OnchainTransactionsScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val viewModel: OnchainTransactionsViewModel = viewModel() + val viewModel: OnchainTransactionsViewModel = rememberViewModel { OnchainTransactionsViewModel() } LaunchedEffect(accountViewModel) { viewModel.init(accountViewModel) @@ -308,15 +308,14 @@ private fun OnchainTransactionItem( val amountSats = view.tx.netValueSats.absoluteValue val formattedAmount = remember(view.tx.netValueSats) { - val fmt = NumberFormat.getIntegerInstance() - (if (isIncoming) "+" else "-") + fmt.format(amountSats) + (if (isIncoming) "+" else "-") + formatGrouped(amountSats) } - val context = LocalContext.current + val monthDayTime = rememberMonthDayTimeFormatter() val dateText = - remember(view.tx.blockTime, view.tx.confirmations, context) { + remember(view.tx.blockTime, view.tx.confirmations, monthDayTime) { val ts = view.tx.blockTime - if (ts != null) formatMonthDayTime(ts, context) else "" + if (ts != null) monthDayTime(ts) else "" } val counterpartyPubkeyHex = view.counterpartyPubkeyHex() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainTransactionsViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/OnchainTransactionsViewModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainTransactionsViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/OnchainTransactionsViewModel.kt index f978af2d63..3ed8baa40b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainTransactionsViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/OnchainTransactionsViewModel.kt @@ -18,12 +18,13 @@ * 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.wallet +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet import androidx.compose.runtime.Immutable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.TransactionFilter import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nipBCOnchainZaps.chain.BitcoinAddressTx @@ -32,6 +33,7 @@ import com.vitorpamplona.quartz.nipBCOnchainZaps.taproot.TaprootAddress import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.OnchainZapEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainZapSendDialog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/OnchainZapSendDialog.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainZapSendDialog.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/OnchainZapSendDialog.kt index 216a41cca9..3a2eb48e96 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainZapSendDialog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/OnchainZapSendDialog.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.wallet +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -117,6 +117,7 @@ import com.vitorpamplona.amethyst.commons.resources.onchain_send_title import com.vitorpamplona.amethyst.commons.resources.onchain_send_to import com.vitorpamplona.amethyst.commons.resources.onchain_send_use_note_split import com.vitorpamplona.amethyst.commons.resources.send +import com.vitorpamplona.amethyst.commons.ui.components.namecoin.NamecoinResolutionRow import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.note.UserPicture @@ -126,9 +127,9 @@ import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.sats import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.bitcoinColor +import com.vitorpamplona.amethyst.commons.util.formatGrouped import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.components.namecoin.NamecoinResolutionRow import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle @@ -140,14 +141,14 @@ import com.vitorpamplona.quartz.nipBCOnchainZaps.builder.OnchainZapBuilder import com.vitorpamplona.quartz.nipBCOnchainZaps.chain.FeeEstimates import com.vitorpamplona.quartz.utils.BigDecimal import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.compose.resources.StringResource -import java.text.NumberFormat /** Shared with the profile Send Payment screen's on-chain rail. */ -internal enum class FeeTier( +enum class FeeTier( val labelRes: StringResource, val etaLabelRes: StringResource, ) { @@ -156,7 +157,7 @@ internal enum class FeeTier( FAST(Res.string.onchain_send_fee_fast, Res.string.onchain_send_fee_eta_fast), } -internal fun FeeEstimates.rateFor(tier: FeeTier): Double = +fun FeeEstimates.rateFor(tier: FeeTier): Double = when (tier) { FeeTier.SLOW -> slowSatPerVbyte FeeTier.NORMAL -> normalSatPerVbyte @@ -706,7 +707,7 @@ private fun AmountSection( if (belowMinimum) { Spacer(Modifier.height(4.dp)) Text( - text = stringRes(Res.string.onchain_send_min_warning, NumberFormat.getNumberInstance().format(MIN_ONCHAIN_ZAP_SATS)), + text = stringRes(Res.string.onchain_send_min_warning, formatGrouped(MIN_ONCHAIN_ZAP_SATS)), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.error, ) @@ -806,7 +807,7 @@ private fun SendButton( modifier = Modifier.size(18.dp), ) Spacer(Modifier.size(8.dp)) - val sats = if (amountSats != null && amountSats > 0) NumberFormat.getNumberInstance().format(amountSats) else null + val sats = if (amountSats != null && amountSats > 0) formatGrouped(amountSats) else null Text( text = when { @@ -866,7 +867,7 @@ private fun SplitsRecipientSection( if (share != null) { val belowDust = share.sats < OnchainZapBuilder.DUST_THRESHOLD_SATS Text( - text = "${NumberFormat.getNumberInstance().format(share.sats)} sats", + text = "${formatGrouped(share.sats)} sats", style = MaterialTheme.typography.bodySmall, color = if (belowDust) { @@ -971,12 +972,12 @@ private fun SuccessBody(result: OnchainZapSendResult.Success) { ResultRow(stringRes(Res.string.onchain_send_result_transaction), result.txid) ResultRow( stringRes(Res.string.onchain_send_result_fee), - stringRes(Res.string.onchain_send_sats_amount, NumberFormat.getNumberInstance().format(result.feeSats)), + stringRes(Res.string.onchain_send_sats_amount, formatGrouped(result.feeSats)), ) if (result.changeSats > 0) { ResultRow( stringRes(Res.string.onchain_send_result_change), - stringRes(Res.string.onchain_send_sats_amount, NumberFormat.getNumberInstance().format(result.changeSats)), + stringRes(Res.string.onchain_send_sats_amount, formatGrouped(result.changeSats)), ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletDetailScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/WalletDetailScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletDetailScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/WalletDetailScreen.kt index 0ae375f82d..5412b9141a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletDetailScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/WalletDetailScreen.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.wallet +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -51,7 +51,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.navigation.Route @@ -62,10 +61,12 @@ import com.vitorpamplona.amethyst.commons.resources.wallet_receive import com.vitorpamplona.amethyst.commons.resources.wallet_sats import com.vitorpamplona.amethyst.commons.resources.wallet_send import com.vitorpamplona.amethyst.commons.resources.wallet_transactions +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.WalletViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.util.formatGrouped import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import java.text.NumberFormat @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -74,7 +75,7 @@ fun WalletDetailScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val walletViewModel: WalletViewModel = viewModel() + val walletViewModel: WalletViewModel = rememberViewModel { WalletViewModel() } walletViewModel.init(accountViewModel) walletViewModel.selectWallet(walletId) @@ -121,8 +122,7 @@ fun WalletDetailScreen( } else { val formattedBalance = remember(balance) { - val fmt = NumberFormat.getIntegerInstance() - fmt.format(balance ?: 0L) + formatGrouped(balance ?: 0L) } Text( text = formattedBalance, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt similarity index 91% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt index 85f789f232..34658fd786 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt @@ -18,11 +18,8 @@ * 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.wallet +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -52,16 +49,16 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res @@ -73,11 +70,16 @@ import com.vitorpamplona.amethyst.commons.resources.wallet_creating_invoice import com.vitorpamplona.amethyst.commons.resources.wallet_description import com.vitorpamplona.amethyst.commons.resources.wallet_receive import com.vitorpamplona.amethyst.commons.resources.wallet_sats +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel +import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.ReceiveState +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.WalletViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.util.formatGrouped import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer -import java.text.NumberFormat +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -86,7 +88,7 @@ fun WalletReceiveScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val walletViewModel: WalletViewModel = viewModel() + val walletViewModel: WalletViewModel = rememberViewModel { WalletViewModel() } LaunchedEffect(accountViewModel, walletId) { walletViewModel.init(accountViewModel) @@ -100,7 +102,8 @@ fun WalletReceiveScreen( val receiveState by walletViewModel.receiveState.collectAsState() var amountText by remember { mutableStateOf("") } var descriptionText by remember { mutableStateOf("") } - val context = LocalContext.current + val clipboard = LocalClipboard.current + val clipboardScope = rememberCoroutineScope() Scaffold( topBar = { @@ -192,8 +195,7 @@ fun WalletReceiveScreen( val formattedAmount = remember(state.amount) { - val fmt = NumberFormat.getIntegerInstance() - fmt.format(state.amount) + formatGrouped(state.amount) } Text( @@ -231,8 +233,7 @@ fun WalletReceiveScreen( ) { OutlinedButton( onClick = { - val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - clipboard.setPrimaryClip(ClipData.newPlainText("invoice", state.invoice)) + clipboardScope.launch { clipboard.setText(state.invoice) } }, modifier = Modifier diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/WalletScreen.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/WalletScreen.kt index 1512dc514d..f92c537ad7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/WalletScreen.kt @@ -18,9 +18,8 @@ * 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.wallet +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet -import android.widget.Toast import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -67,12 +66,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.clink.ui.ClinkBudgetDialog import com.vitorpamplona.amethyst.commons.hashtags.Cashu import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons @@ -103,15 +100,19 @@ import com.vitorpamplona.amethyst.commons.resources.wallet_rename_title import com.vitorpamplona.amethyst.commons.resources.wallet_sats import com.vitorpamplona.amethyst.commons.resources.wallet_save import com.vitorpamplona.amethyst.commons.resources.wallet_set_default +import com.vitorpamplona.amethyst.commons.ui.components.rememberLongNotice +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.CashuWalletViewModel +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.WalletInfo +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.WalletViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.util.formatGrouped import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.experimental.clink.debits.DebitFrequency import kotlinx.coroutines.launch -import java.text.NumberFormat import androidx.compose.material3.Icon as Material3Icon @OptIn(ExperimentalMaterial3Api::class) @@ -120,10 +121,10 @@ fun WalletScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val walletViewModel: WalletViewModel = viewModel() + val walletViewModel: WalletViewModel = rememberViewModel { WalletViewModel() } walletViewModel.init(accountViewModel) - val cashuWalletViewModel: CashuWalletViewModel = viewModel() + val cashuWalletViewModel: CashuWalletViewModel = rememberViewModel { CashuWalletViewModel() } // Synchronous init so state-flow getters don't hit a null `account` on // the first composition pass. init() is idempotent — just sets refs. cashuWalletViewModel.init(accountViewModel) @@ -246,7 +247,7 @@ private fun MultiWalletHomeContent( cashuMintCount: Int, ) { val walletInfoList by walletViewModel.walletInfoList.collectAsState() - val context = LocalContext.current + val longNotice = rememberLongNotice() val budgetApprovedMsg = stringRes(Res.string.clink_budget_approved) val debitNoResponseMsg = stringRes(Res.string.clink_debit_no_response) @@ -296,7 +297,7 @@ private fun MultiWalletHomeContent( !error.isNullOrBlank() -> error else -> debitNoResponseMsg } - Toast.makeText(context, msg, Toast.LENGTH_LONG).show() + longNotice.show(msg) } } } else { @@ -458,8 +459,7 @@ private fun WalletCard( Column(horizontalAlignment = Alignment.End) { val formattedBalance = remember(walletInfo.balanceSats) { - val fmt = NumberFormat.getIntegerInstance() - fmt.format(walletInfo.balanceSats ?: 0L) + formatGrouped(walletInfo.balanceSats ?: 0L) } Text( text = formattedBalance, @@ -475,10 +475,10 @@ private fun WalletCard( } } - if (walletInfo.error != null) { + walletInfo.error?.let { error -> Spacer(modifier = Modifier.height(4.dp)) Text( - text = walletInfo.error, + text = error, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall, ) @@ -572,7 +572,7 @@ private fun CashuWalletRow( onClick: () -> Unit, ) { val formattedBalance = - remember(balanceSats) { NumberFormat.getIntegerInstance().format(balanceSats) } + remember(balanceSats) { formatGrouped(balanceSats) } Card( modifier = Modifier diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletSendScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/WalletSendScreen.kt similarity index 91% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletSendScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/WalletSendScreen.kt index b0d036312a..0e8cf144d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletSendScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/WalletSendScreen.kt @@ -18,10 +18,8 @@ * 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.wallet +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet -import android.content.ClipboardManager -import android.content.Context import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -47,13 +45,13 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res @@ -63,9 +61,14 @@ import com.vitorpamplona.amethyst.commons.resources.wallet_pay import com.vitorpamplona.amethyst.commons.resources.wallet_payment_sending import com.vitorpamplona.amethyst.commons.resources.wallet_payment_success import com.vitorpamplona.amethyst.commons.resources.wallet_send +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel +import com.vitorpamplona.amethyst.commons.ui.components.util.getText import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.SendState +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.WalletViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -74,7 +77,7 @@ fun WalletSendScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val walletViewModel: WalletViewModel = viewModel() + val walletViewModel: WalletViewModel = rememberViewModel { WalletViewModel() } LaunchedEffect(accountViewModel, walletId) { walletViewModel.init(accountViewModel) @@ -87,7 +90,8 @@ fun WalletSendScreen( val sendState by walletViewModel.sendState.collectAsState() var invoiceText by remember { mutableStateOf("") } - val context = LocalContext.current + val clipboard = LocalClipboard.current + val clipboardScope = rememberCoroutineScope() Scaffold( topBar = { @@ -126,11 +130,7 @@ fun WalletSendScreen( maxLines = 5, trailingIcon = { IconButton(onClick = { - val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = clipboard.primaryClip - if (clip != null && clip.itemCount > 0) { - invoiceText = clip.getItemAt(0).text?.toString() ?: "" - } + clipboardScope.launch { clipboard.getText()?.let { invoiceText = it } } }) { Icon( symbol = MaterialSymbols.ContentPaste, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt index f1bdad6aa6..90cc9e89ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/WalletTransactionsScreen.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.wallet +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -51,11 +51,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.navigation.Route @@ -74,16 +72,19 @@ import com.vitorpamplona.amethyst.commons.resources.wallet_refresh import com.vitorpamplona.amethyst.commons.resources.wallet_transactions import com.vitorpamplona.amethyst.commons.resources.wallet_transactions_load_failed import com.vitorpamplona.amethyst.commons.ui.components.EmptyState +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.LoadUser import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay +import com.vitorpamplona.amethyst.commons.ui.note.rememberMonthDayTimeFormatter +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.TransactionFilter +import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.WalletViewModel import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.util.formatGrouped import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.note.formatMonthDayTime import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransaction import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransactionType -import java.text.NumberFormat @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -92,7 +93,7 @@ fun WalletTransactionsScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val walletViewModel: WalletViewModel = viewModel() + val walletViewModel: WalletViewModel = rememberViewModel { WalletViewModel() } LaunchedEffect(accountViewModel, walletId) { walletViewModel.init(accountViewModel) @@ -276,14 +277,13 @@ private fun TransactionItem( val amountSats = (tx.amount ?: 0L) / 1000L val formattedAmount = remember(amountSats) { - val fmt = NumberFormat.getIntegerInstance() - (if (isIncoming) "+" else "-") + fmt.format(amountSats) + (if (isIncoming) "+" else "-") + formatGrouped(amountSats) } - val context = LocalContext.current + val monthDayTime = rememberMonthDayTimeFormatter() val dateText = - remember(tx.created_at, context) { - tx.created_at?.let { formatMonthDayTime(it, context) } ?: "" + remember(tx.created_at, monthDayTime) { + tx.created_at?.let { monthDayTime(it) } ?: "" } val directionLabel = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/WalletViewModel.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/WalletViewModel.kt index 317d0b09be..c93d217a27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/WalletViewModel.kt @@ -18,11 +18,10 @@ * 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.wallet +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.clink.ClinkDebitWalletEntryNorm import com.vitorpamplona.amethyst.commons.model.nip47WalletConnect.NwcSignerState @@ -68,6 +67,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -78,6 +78,8 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull import org.jetbrains.compose.resources.StringResource +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid sealed class SendState { data object Idle : SendState() @@ -130,11 +132,7 @@ private const val NWC_TIMEOUT_MS = 30_000L class WalletViewModel : ViewModel() { // Error text is resolved here rather than in the screens because a single _error - // flow feeds several of them. WalletViewModel only ever runs in the main process, - // so Amethyst.instance is always initialised (see AccountViewModel for the same - // pattern); the :napplet process never builds a wallet screen. - private val appContext get() = Amethyst.instance.appContext - + // flow feeds several of them. private suspend fun text(id: StringResource) = loadStringRes(id) private suspend fun text( @@ -338,6 +336,7 @@ class WalletViewModel : ViewModel() { } /** Adds a CLINK debit pointer (`ndebit1…`) as a spend-only payment source. */ + @OptIn(ExperimentalUuidApi::class) fun addClinkDebitWallet( name: String, ndebit: String, @@ -347,9 +346,7 @@ class WalletViewModel : ViewModel() { val entry = ClinkDebitWalletEntryNorm( id = - java.util.UUID - .randomUUID() - .toString(), + Uuid.random().toString(), name = name.ifBlank { "Debit" }, pointer = pointer, ) @@ -371,12 +368,13 @@ class WalletViewModel : ViewModel() { ) { val acc = account ?: return val pointer = _debitWallets.value.firstOrNull { it.id == walletId }?.pointer ?: return + val moneyOpRelays = accountViewModel?.host?.moneyOpRelays ?: return viewModelScope.launch { // A malformed budget (e.g. an out-of-spec frequency unit) makes requestBudget throw; // treat it as "no response" so the dialog dismisses instead of hanging on a spinner. val response = try { - ClinkDebitPayer.requestBudget(acc, Amethyst.instance.torEvaluatorFlow, pointer, amountSats, frequency) + ClinkDebitPayer.requestBudget(acc, moneyOpRelays, pointer, amountSats, frequency) } catch (_: IllegalArgumentException) { null } @@ -384,6 +382,7 @@ class WalletViewModel : ViewModel() { } } + @OptIn(ExperimentalUuidApi::class) fun addWallet( name: String, uri: Nip47WalletConnect.Nip47URINorm, @@ -392,9 +391,7 @@ class WalletViewModel : ViewModel() { val entry = NwcWalletEntryNorm( id = - java.util.UUID - .randomUUID() - .toString(), + Uuid.random().toString(), name = name.ifBlank { "Wallet" }, uri = uri, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/datasource/OnchainZapsFilterAssemblerSubscription.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/datasource/OnchainZapsFilterAssemblerSubscription.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/datasource/OnchainZapsFilterAssemblerSubscription.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/datasource/OnchainZapsFilterAssemblerSubscription.kt index 247c2fdb01..e884350bed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/datasource/OnchainZapsFilterAssemblerSubscription.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/datasource/OnchainZapsFilterAssemblerSubscription.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.wallet.datasource +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.datasource import androidx.compose.runtime.Composable import androidx.compose.runtime.remember diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/wizard/CashuWalletWizardScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/wizard/CashuWalletWizardScreen.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/wizard/CashuWalletWizardScreen.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/wizard/CashuWalletWizardScreen.kt index 8f3bc76fa8..4edb6d454a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/wizard/CashuWalletWizardScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/wizard/CashuWalletWizardScreen.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.wallet.wizard +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.wizard import androidx.compose.animation.AnimatedContent import androidx.compose.animation.core.animateFloatAsState @@ -67,7 +67,6 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.navigation.Route @@ -99,10 +98,11 @@ import com.vitorpamplona.amethyst.commons.resources.cashu_wizard_single_descript import com.vitorpamplona.amethyst.commons.resources.cashu_wizard_single_title import com.vitorpamplona.amethyst.commons.resources.cashu_wizard_title import com.vitorpamplona.amethyst.commons.resources.cashu_wizard_use_wallet +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.util.formatGrouped import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import java.text.NumberFormat /** * Find-or-create wizard for the Cashu wallet. Reached from the wallet screen's @@ -122,7 +122,7 @@ fun CashuWalletWizardScreen( accountViewModel: AccountViewModel, nav: INav, ) { - val viewModel: CashuWalletWizardViewModel = viewModel() + val viewModel: CashuWalletWizardViewModel = rememberViewModel { CashuWalletWizardViewModel() } viewModel.init(accountViewModel) val wizardState by viewModel.wizardState.collectAsState() @@ -227,7 +227,7 @@ fun CashuWalletWizardScreen( } } -private fun formatSats(sats: Long): String = NumberFormat.getInstance().format(sats) +private fun formatSats(sats: Long): String = formatGrouped(sats) /** * Animate a sats figure counting up from zero the first time it's shown, then diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/wizard/CashuWalletWizardViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/wizard/CashuWalletWizardViewModel.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/wizard/CashuWalletWizardViewModel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/wizard/CashuWalletWizardViewModel.kt index 37fec79deb..a76231e1ba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/wizard/CashuWalletWizardViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/wallet/wizard/CashuWalletWizardViewModel.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.wallet.wizard +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.wallet.wizard import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/settings/SettingsCatalog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/settings/SettingsCatalog.kt index 5260295216..743b2ff564 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/settings/SettingsCatalog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/settings/SettingsCatalog.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.commons.ui.settings import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol +import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.StringResource /** Leading-icon representation mirroring the two [SettingsItem] overloads. */ @@ -30,7 +31,7 @@ sealed interface SettingsIcon { ) : SettingsIcon data class Painter( - val iconPainter: Int, + val iconPainter: DrawableResource, val iconPainterRef: Int, ) : SettingsIcon } diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/uploads/UploadErrorText.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/uploads/UploadErrorText.kt new file mode 100644 index 0000000000..6ffa633dda --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/uploads/UploadErrorText.kt @@ -0,0 +1,52 @@ +/* + * 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.commons.ui.uploads + +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed +import com.vitorpamplona.amethyst.commons.resources.blossom_payment_required +import com.vitorpamplona.amethyst.commons.resources.could_not_check_downloaded_file +import com.vitorpamplona.amethyst.commons.resources.could_not_download_from_the_server +import com.vitorpamplona.amethyst.commons.resources.could_not_open_the_compressed_file +import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media +import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_upload +import com.vitorpamplona.amethyst.commons.resources.media_too_big_for_nip95 +import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading +import com.vitorpamplona.amethyst.commons.resources.upload_cancelled +import com.vitorpamplona.amethyst.commons.service.uploads.UploadError +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState +import org.jetbrains.compose.resources.StringResource + +/** The message template for this upload failure; fill it with [UploadingState.Error.params]. */ +val UploadingState.Error.errorResource: StringResource + get() = + when (error) { + UploadError.MEDIA_TOO_BIG_FOR_NIP95 -> Res.string.media_too_big_for_nip95 + UploadError.COULD_NOT_CHECK_DOWNLOADED_FILE -> Res.string.could_not_check_downloaded_file + UploadError.COULD_NOT_OPEN_COMPRESSED_FILE -> Res.string.could_not_open_the_compressed_file + UploadError.LOGIN_WITH_PRIVATE_KEY -> Res.string.login_with_a_private_key_to_be_able_to_upload + UploadError.FAILED_TO_UPLOAD_MEDIA -> Res.string.failed_to_upload_media + UploadError.BLOSSOM_PAYMENT_REQUIRED -> Res.string.blossom_payment_required + UploadError.SERVER_DID_NOT_PROVIDE_URL -> Res.string.server_did_not_provide_a_url_after_uploading + UploadError.COULD_NOT_DOWNLOAD_FROM_SERVER -> Res.string.could_not_download_from_the_server + UploadError.AVIF_METADATA_STRIP_FAILED -> Res.string.avif_metadata_strip_failed + UploadError.UPLOAD_CANCELLED -> Res.string.upload_cancelled + } diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/uploads/UploadToDefaultServer.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/uploads/UploadToDefaultServer.kt new file mode 100644 index 0000000000..4e14ab5b59 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/uploads/UploadToDefaultServer.kt @@ -0,0 +1,104 @@ +/* + * 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.commons.ui.uploads + +import com.vitorpamplona.amethyst.commons.model.Account +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details +import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_upload +import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title +import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload_cancelled +import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading +import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.UploadError +import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState +import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import kotlinx.coroutines.CancellationException + +/** + * Compresses, strips (when [stripMetadata] and the account ask for it) and uploads one picked file + * to the account's default media server, returning the hosted URL. Every failure, a + * metadata-stripping one included, is reported through [onError] with a user-facing title and + * message, and yields null. + * + * For single-image fields (avatars, banners, pack and cover images) that only need a URL back. + */ +suspend fun uploadToDefaultServer( + media: SelectedMedia, + account: Account, + uploader: MediaUploader, + onError: (title: String, message: String) -> Unit, + quality: CompressorQuality = CompressorQuality.MEDIUM, + stripMetadata: Boolean = true, +): String? { + val state = + try { + UploadOrchestrator().upload( + uri = media.uri, + mimeType = media.mimeType, + alt = null, + contentWarningReason = null, + compressionQuality = quality, + server = account.settings.defaultFileServer, + account = account, + uploader = uploader, + stripMetadata = stripMetadata && account.settings.stripLocationOnUpload, + onStrippingFailed = { false }, + ) + } catch (_: SignerExceptions.ReadOnlyException) { + onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.login_with_a_private_key_to_be_able_to_upload)) + return null + } catch (e: Exception) { + if (e is CancellationException) throw e + onError(loadStringRes(Res.string.failed_to_upload_media_no_details), e.message ?: e::class.simpleName ?: "") + return null + } + + return when (state) { + is UploadingState.Finished -> { + val uploaded = state.result + if (uploaded is UploadOrchestrator.OrchestratorResult.ServerResult) { + uploaded.url + } else { + onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(Res.string.server_did_not_provide_a_url_after_uploading)) + null + } + } + + is UploadingState.Error -> { + when (state.error) { + // The only cancel on this path is a metadata strip that failed: say so, since the + // user turned stripping on and would otherwise not know why nothing uploaded. + UploadError.UPLOAD_CANCELLED -> + onError(loadStringRes(Res.string.metadata_strip_failed_title), loadStringRes(Res.string.metadata_strip_failed_upload_cancelled)) + UploadError.AVIF_METADATA_STRIP_FAILED -> + onError(loadStringRes(Res.string.metadata_strip_failed_title), loadStringRes(state.errorResource, *state.params)) + else -> + onError(loadStringRes(Res.string.failed_to_upload_media_no_details), loadStringRes(state.errorResource, *state.params)) + } + null + } + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.kt new file mode 100644 index 0000000000..3b37a76947 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.kt @@ -0,0 +1,40 @@ +/* + * 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.commons.ui.wallet + +import androidx.compose.runtime.Composable + +/** Hands a payment to a wallet app installed on the device. */ +fun interface WalletAppLauncher { + /** + * Opens [invoice] (BOLT-11) in the wallet app. [onPaid] runs once the wallet took it, which is + * not a payment confirmation; [onError] gets [noWalletFound] when no wallet can open it. + */ + fun payInvoice( + invoice: String, + noWalletFound: String, + onPaid: () -> Unit, + onError: (String) -> Unit, + ) +} + +@Composable +expect fun rememberWalletAppLauncher(): WalletAppLauncher diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountFeedContentStates.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountFeedContentStates.kt index 3242eeb674..2fab0a5664 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountFeedContentStates.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountFeedContentStates.kt @@ -373,8 +373,6 @@ class AccountFeedContentStates( } fun updateFeedsWith(newNotes: Set) { - LocalCache.appHost.assertNotMainThread() - homeLive.updateFeedWith(newNotes) homeNewThreads.updateFeedWith(newNotes) homeReplies.updateFeedWith(newNotes) @@ -447,8 +445,6 @@ class AccountFeedContentStates( } fun deleteNotes(newNotes: Set) { - LocalCache.appHost.assertNotMainThread() - homeLive.deleteFromFeed(newNotes) homeNewThreads.deleteFromFeed(newNotes) homeReplies.deleteFromFeed(newNotes) diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModel.kt index b72017d6d4..5f6b424ee1 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModel.kt @@ -729,8 +729,6 @@ class AccountViewModel( accountChoices: LiveHiddenUsers, followUsers: Set, ): NoteComposeReportState { - LocalCache.appHost.assertNotMainThread() - val isFromLoggedIn = note.author?.pubkeyHex == userProfile().pubkeyHex val isFromLoggedInFollow = note.author?.let { followUsers.contains(it.pubkeyHex) } ?: true val isPostHidden = note.isHiddenFor(accountChoices) diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/FileSelect.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/FileSelect.ios.kt new file mode 100644 index 0000000000..766d009d92 --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/FileSelect.ios.kt @@ -0,0 +1,42 @@ +/* + * 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.commons.ui.actions.uploads + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** No document picker wired yet: reports a cancel. */ +@Composable +actual fun FileSelect(onFilesSelected: (ImmutableList) -> Unit) { + LaunchedEffect(Unit) { onFilesSelected(persistentListOf()) } +} + +/** No document picker wired yet: reports a cancel. */ +@Composable +actual fun DocumentSelectSingle( + mimeTypes: List, + onPicked: (SelectedMedia?) -> Unit, +) { + LaunchedEffect(Unit) { onPicked(null) } +} diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/GallerySelect.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/GallerySelect.ios.kt new file mode 100644 index 0000000000..ec3612ac65 --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/GallerySelect.ios.kt @@ -0,0 +1,42 @@ +/* + * 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.commons.ui.actions.uploads + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +// No picker wired on this platform yet: report a cancel so callers close their picker state. + +@Composable +actual fun GallerySelect(onImageUri: (ImmutableList) -> Unit) { + LaunchedEffect(Unit) { onImageUri(persistentListOf()) } +} + +@Composable +actual fun GallerySelectSingle( + imagesOnly: Boolean, + onImageUri: (SelectedMedia?) -> Unit, +) { + LaunchedEffect(Unit) { onImageUri(null) } +} diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VideoThumbnail.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VideoThumbnail.ios.kt new file mode 100644 index 0000000000..3e746b9968 --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VideoThumbnail.ios.kt @@ -0,0 +1,29 @@ +/* + * 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.commons.ui.actions.uploads + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ImageBitmap +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia + +/** No frame decoder wired yet: the preview plays the video instead. */ +@Composable +actual fun rememberVideoThumbnail(media: SelectedMedia): ImageBitmap? = null diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.ios.kt new file mode 100644 index 0000000000..43da45fe05 --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.ios.kt @@ -0,0 +1,26 @@ +/* + * 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.commons.ui.components + +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri + +actual fun Modifier.imageContentReceiver(onImage: (MediaUri, String?) -> Unit): Modifier = this diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/RememberViewModel.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/RememberViewModel.ios.kt index 089cdea849..9b52253b2f 100644 --- a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/RememberViewModel.ios.kt +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/RememberViewModel.ios.kt @@ -26,7 +26,6 @@ import androidx.compose.runtime.remember import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStore -import androidx.lifecycle.viewmodel.CreationExtras import kotlin.reflect.KClass /** @@ -50,15 +49,4 @@ actual fun rememberViewModel( modelClass: KClass, key: String?, factory: () -> VM, -): VM = - rememberViewModel( - modelClass, - key, - object : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create( - modelClass: KClass, - extras: CreationExtras, - ): T = factory() as T - }, - ) +): VM = rememberViewModel(modelClass, key, rememberLambdaFactory(factory)) diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/SetDialogToEdgeToEdge.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/SetDialogToEdgeToEdge.ios.kt new file mode 100644 index 0000000000..874770bc15 --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/SetDialogToEdgeToEdge.ios.kt @@ -0,0 +1,37 @@ +/* + * 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.commons.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.window.DialogProperties + +@Composable +actual fun SetDialogToEdgeToEdge() {} + +actual fun edgeToEdgeDialogProperties( + dismissOnBackPress: Boolean, + dismissOnClickOutside: Boolean, +): DialogProperties = + DialogProperties( + usePlatformDefaultWidth = false, + dismissOnBackPress = dismissOnBackPress, + dismissOnClickOutside = dismissOnClickOutside, + ) diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.ios.kt index a991b09cfb..b0d73e3abf 100644 --- a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.ios.kt +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.ios.kt @@ -27,3 +27,6 @@ private val loggedNotice = ShortNotice { message -> Log.d("ShortNotice") { messa @Composable actual fun rememberShortNotice(): ShortNotice = loggedNotice + +@Composable +actual fun rememberLongNotice(): ShortNotice = loggedNotice diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/MonthDayTime.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/MonthDayTime.ios.kt new file mode 100644 index 0000000000..f03895ab70 --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/MonthDayTime.ios.kt @@ -0,0 +1,30 @@ +/* + * 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.commons.ui.note + +import androidx.compose.runtime.Composable + +private val monthDayTime = DateSkeletonFormatter("MMMdjm") + +private val formatMonthDayTime: (Long) -> String = { epochSeconds -> monthDayTime.format(epochSeconds * 1000L) } + +@Composable +actual fun rememberMonthDayTimeFormatter(): (epochSeconds: Long) -> String = formatMonthDayTime diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/EmbeddedAppSurfaces.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/EmbeddedAppSurfaces.ios.kt new file mode 100644 index 0000000000..e5cd71284d --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/EmbeddedAppSurfaces.ios.kt @@ -0,0 +1,23 @@ +/* + * 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.commons.ui.platform + +actual val supportsEmbeddedAppTabs: Boolean = false diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.ios.kt new file mode 100644 index 0000000000..a144adff19 --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.ios.kt @@ -0,0 +1,27 @@ +/* + * 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.commons.ui.screen.loggedIn.qrcode + +import androidx.compose.runtime.Composable + +/** Not wired on iOS yet. */ +@Composable +actual fun KeepScreenBrightAndAwake() {} diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrMatrix.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrMatrix.ios.kt new file mode 100644 index 0000000000..a7052a9868 --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrMatrix.ios.kt @@ -0,0 +1,24 @@ +/* + * 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.commons.ui.screen.loggedIn.qrcode + +/** No encoder on iOS yet: the drawer shows its blank frame. */ +actual fun encodeQrMatrix(contents: String): QrMatrix? = null diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.ios.kt new file mode 100644 index 0000000000..9ecd3a05f9 --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.ios.kt @@ -0,0 +1,43 @@ +/* + * 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.commons.ui.wallet + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalUriHandler +import kotlin.coroutines.cancellation.CancellationException + +/** Opens the `lightning:` link with whatever the system registered for it. */ +@Composable +actual fun rememberWalletAppLauncher(): WalletAppLauncher { + val uriHandler = LocalUriHandler.current + return remember(uriHandler) { + WalletAppLauncher { invoice, noWalletFound, onPaid, onError -> + try { + uriHandler.openUri("lightning:$invoice") + onPaid() + } catch (e: Exception) { + if (e is CancellationException) throw e + onError(noWalletFound) + } + } + } +} diff --git a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/components/RememberViewModel.jvmAndroid.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/components/RememberViewModel.jvmAndroid.kt index 7faaa0e1ee..e09bb2ea16 100644 --- a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/components/RememberViewModel.jvmAndroid.kt +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/components/RememberViewModel.jvmAndroid.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.runtime.Composable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.compose.viewModel import kotlin.reflect.KClass @@ -39,16 +38,4 @@ actual fun rememberViewModel( modelClass: KClass, key: String?, factory: () -> VM, -): VM = - viewModel( - modelClass = modelClass, - key = key, - factory = - object : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create( - modelClass: KClass, - extras: CreationExtras, - ): T = factory() as T - }, - ) +): VM = viewModel(modelClass = modelClass, key = key, factory = rememberLambdaFactory(factory)) diff --git a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrMatrix.jvmAndroid.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrMatrix.jvmAndroid.kt new file mode 100644 index 0000000000..2c60a855c0 --- /dev/null +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/QrMatrix.jvmAndroid.kt @@ -0,0 +1,52 @@ +/* + * 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.commons.ui.screen.loggedIn.qrcode + +import com.google.zxing.EncodeHintType +import com.google.zxing.WriterException +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import com.google.zxing.qrcode.encoder.Encoder + +actual fun encodeQrMatrix(contents: String): QrMatrix? { + if (contents.isEmpty()) return null + + val code = + try { + Encoder.encode( + contents, + ErrorCorrectionLevel.Q, + mapOf( + EncodeHintType.CHARACTER_SET to "UTF-8", + EncodeHintType.MARGIN to QR_QUIET_ZONE_MODULES, + EncodeHintType.ERROR_CORRECTION to ErrorCorrectionLevel.Q, + ), + ) + } catch (e: WriterException) { + return null + } + + val matrix = code.matrix + return QrMatrix( + width = matrix.width, + height = matrix.height, + modules = BooleanArray(matrix.width * matrix.height) { matrix[it % matrix.width, it / matrix.width] == 1.toByte() }, + ) +} diff --git a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelPreviews.jvmAndroid.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelPreviews.jvmAndroid.kt index 4c9a3d0b40..4ac1c41d37 100644 --- a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelPreviews.jvmAndroid.kt +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/AccountViewModelPreviews.jvmAndroid.kt @@ -37,8 +37,12 @@ import com.vitorpamplona.amethyst.commons.service.http.EmptyRoleBasedHttpClientB import com.vitorpamplona.amethyst.commons.service.http.EncryptionKeyCache import com.vitorpamplona.amethyst.commons.service.lnurl.LnurlHttpTransport import com.vitorpamplona.amethyst.commons.service.pow.PoWJobFailure +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader +import com.vitorpamplona.amethyst.commons.service.uploads.UnavailableMediaUploader import com.vitorpamplona.amethyst.commons.state.UiSettingsState import com.vitorpamplona.amethyst.commons.tor.MoneyOpRelayRouting +import com.vitorpamplona.amethyst.commons.tor.TorRelayEvaluation +import com.vitorpamplona.amethyst.commons.tor.TorRelaySettings import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow import com.vitorpamplona.amethyst.commons.tor.TorType import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -46,15 +50,20 @@ import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.relay.client.EmptyNostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker import com.vitorpamplona.quartz.nip01Core.relay.client.auth.EmptyIAuthStatus +import com.vitorpamplona.quartz.nip01Core.relay.client.auth.RelayAuthSnapshot import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip03Timestamp.EmptyOtsResolverBuilder import com.vitorpamplona.quartz.nip05DnsIdentifiers.EmptyNip05Client import com.vitorpamplona.quartz.nip60Cashu.mintApi.OkHttpMintTransport import com.vitorpamplona.quartz.utils.Hex +import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.persistentMapOf import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf import okhttp3.OkHttpClient @@ -189,6 +198,9 @@ private object PreviewAccountViewModelHost : AccountViewModelHost { override val localBlossomCacheAvailable: Flow = flowOf(false) override val powPublishFailures: Flow = emptyFlow() override val relayStats = RelayStats(EmptyNostrClient()) + override val torRelayEvaluation: StateFlow = MutableStateFlow(TorRelayEvaluation(TorRelaySettings())) + override val relayAuthState: StateFlow> = MutableStateFlow(persistentMapOf()) + override val mediaUploader: MediaUploader = UnavailableMediaUploader override val websocketBuilder = BasicOkHttpWebSocket.Builder { OkHttpClient() } override val lnurlTransport = LnurlHttpTransport { throw IllegalStateException("Previews do not reach the network") } override val moneyOpRelays = MoneyOpRelayRouting.None diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/FileSelect.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/FileSelect.jvm.kt new file mode 100644 index 0000000000..766d009d92 --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/FileSelect.jvm.kt @@ -0,0 +1,42 @@ +/* + * 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.commons.ui.actions.uploads + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** No document picker wired yet: reports a cancel. */ +@Composable +actual fun FileSelect(onFilesSelected: (ImmutableList) -> Unit) { + LaunchedEffect(Unit) { onFilesSelected(persistentListOf()) } +} + +/** No document picker wired yet: reports a cancel. */ +@Composable +actual fun DocumentSelectSingle( + mimeTypes: List, + onPicked: (SelectedMedia?) -> Unit, +) { + LaunchedEffect(Unit) { onPicked(null) } +} diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/GallerySelect.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/GallerySelect.jvm.kt new file mode 100644 index 0000000000..ec3612ac65 --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/GallerySelect.jvm.kt @@ -0,0 +1,42 @@ +/* + * 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.commons.ui.actions.uploads + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +// No picker wired on this platform yet: report a cancel so callers close their picker state. + +@Composable +actual fun GallerySelect(onImageUri: (ImmutableList) -> Unit) { + LaunchedEffect(Unit) { onImageUri(persistentListOf()) } +} + +@Composable +actual fun GallerySelectSingle( + imagesOnly: Boolean, + onImageUri: (SelectedMedia?) -> Unit, +) { + LaunchedEffect(Unit) { onImageUri(null) } +} diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VideoThumbnail.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VideoThumbnail.jvm.kt new file mode 100644 index 0000000000..3e746b9968 --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/VideoThumbnail.jvm.kt @@ -0,0 +1,29 @@ +/* + * 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.commons.ui.actions.uploads + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ImageBitmap +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia + +/** No frame decoder wired yet: the preview plays the video instead. */ +@Composable +actual fun rememberVideoThumbnail(media: SelectedMedia): ImageBitmap? = null diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.jvm.kt new file mode 100644 index 0000000000..43da45fe05 --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ImageContentReceiver.jvm.kt @@ -0,0 +1,26 @@ +/* + * 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.commons.ui.components + +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri + +actual fun Modifier.imageContentReceiver(onImage: (MediaUri, String?) -> Unit): Modifier = this diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/SetDialogToEdgeToEdge.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/SetDialogToEdgeToEdge.jvm.kt new file mode 100644 index 0000000000..874770bc15 --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/SetDialogToEdgeToEdge.jvm.kt @@ -0,0 +1,37 @@ +/* + * 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.commons.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.window.DialogProperties + +@Composable +actual fun SetDialogToEdgeToEdge() {} + +actual fun edgeToEdgeDialogProperties( + dismissOnBackPress: Boolean, + dismissOnClickOutside: Boolean, +): DialogProperties = + DialogProperties( + usePlatformDefaultWidth = false, + dismissOnBackPress = dismissOnBackPress, + dismissOnClickOutside = dismissOnClickOutside, + ) diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.jvm.kt index a991b09cfb..b0d73e3abf 100644 --- a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.jvm.kt +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ShortNotice.jvm.kt @@ -27,3 +27,6 @@ private val loggedNotice = ShortNotice { message -> Log.d("ShortNotice") { messa @Composable actual fun rememberShortNotice(): ShortNotice = loggedNotice + +@Composable +actual fun rememberLongNotice(): ShortNotice = loggedNotice diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/MonthDayTime.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/MonthDayTime.jvm.kt new file mode 100644 index 0000000000..f03895ab70 --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/MonthDayTime.jvm.kt @@ -0,0 +1,30 @@ +/* + * 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.commons.ui.note + +import androidx.compose.runtime.Composable + +private val monthDayTime = DateSkeletonFormatter("MMMdjm") + +private val formatMonthDayTime: (Long) -> String = { epochSeconds -> monthDayTime.format(epochSeconds * 1000L) } + +@Composable +actual fun rememberMonthDayTimeFormatter(): (epochSeconds: Long) -> String = formatMonthDayTime diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/EmbeddedAppSurfaces.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/EmbeddedAppSurfaces.jvm.kt new file mode 100644 index 0000000000..e5cd71284d --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/platform/EmbeddedAppSurfaces.jvm.kt @@ -0,0 +1,23 @@ +/* + * 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.commons.ui.platform + +actual val supportsEmbeddedAppTabs: Boolean = false diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.jvm.kt new file mode 100644 index 0000000000..3412c7355b --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/qrcode/KeepScreenBrightAndAwake.jvm.kt @@ -0,0 +1,27 @@ +/* + * 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.commons.ui.screen.loggedIn.qrcode + +import androidx.compose.runtime.Composable + +/** Desktop monitors do not dim for a QR code the way a phone does; nothing to pin. */ +@Composable +actual fun KeepScreenBrightAndAwake() {} diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.jvm.kt new file mode 100644 index 0000000000..9ecd3a05f9 --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/wallet/WalletAppLauncher.jvm.kt @@ -0,0 +1,43 @@ +/* + * 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.commons.ui.wallet + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalUriHandler +import kotlin.coroutines.cancellation.CancellationException + +/** Opens the `lightning:` link with whatever the system registered for it. */ +@Composable +actual fun rememberWalletAppLauncher(): WalletAppLauncher { + val uriHandler = LocalUriHandler.current + return remember(uriHandler) { + WalletAppLauncher { invoice, noWalletFound, onPaid, onError -> + try { + uriHandler.openUri("lightning:$invoice") + onPaid() + } catch (e: Exception) { + if (e is CancellationException) throw e + onError(noWalletFound) + } + } + } +} diff --git a/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/QueryParametersTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/QueryParametersTest.kt new file mode 100644 index 0000000000..b3b57ae7ca --- /dev/null +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/QueryParametersTest.kt @@ -0,0 +1,53 @@ +/* + * 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.commons.ui.navigation + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class QueryParametersTest { + @Test + fun readsOpaqueAndHierarchicalQueries() { + assertEquals("npub1abc", "marmot:0011?account=npub1abc".findQueryParameterValue("account")) + assertEquals("30078:ab:x", "connectedapp?coordinate=30078:ab:x&y=1".findQueryParameterValue("coordinate")) + assertEquals("1", "https://h/p?a=0&y=1".findQueryParameterValue("y")) + } + + @Test + fun dropsTheFragment() { + assertEquals("abc", "connectedapp?coordinate=abc#x".findQueryParameterValue("coordinate")) + assertNull("connectedapp#x?coordinate=abc".findQueryParameterValue("coordinate")) + } + + @Test + fun keepsEqualsInsideValues() { + assertEquals("https://h/p?q=1", "url?id=https://h/p?q=1".findQueryParameterValue("id")) + assertEquals("a=b", "x?k=a=b".findQueryParameterValue("k")) + } + + @Test + fun missingOrEmptyIsNull() { + assertNull("nevent1xyz".findQueryParameterValue("id")) + assertNull("x?id=".findQueryParameterValue("id")) + assertNull("x?other=1".findQueryParameterValue("id")) + } +} diff --git a/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/note/DateSkeletonFormatterTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/note/DateSkeletonFormatterTest.kt index e95be9295e..3d3f110330 100644 --- a/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/note/DateSkeletonFormatterTest.kt +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/note/DateSkeletonFormatterTest.kt @@ -75,6 +75,17 @@ class DateSkeletonFormatterTest { assertEquals("Jan 2024", DateSkeletonFormatter("yMMM").format(millis(2024, 1, 5))) } + @Test + fun monthDayTimeSkeletonIsOnePattern() { + // rememberMonthDayTimeFormatter's JVM skeleton: `j` must be accepted, and the locale's + // own separator kept between the date and the time. + Locale.setDefault(Locale.US) + val us = DateSkeletonFormatter("MMMdjm").format(millis(2024, 5, 28, hour = 14)) + assertTrue(us, us.startsWith("May 28,") && us.contains("2:00") && us.contains("PM")) + Locale.setDefault(Locale.UK) + assertTrue(DateSkeletonFormatter("MMMdjm").format(millis(2024, 5, 28, hour = 14)).contains("14:00")) + } + @Test fun calendarDayDistinguishesDaysAndYears() { assertEquals(calendarYearAndDay(millis(2024, 3, 1, hour = 1)), calendarYearAndDay(millis(2024, 3, 1, hour = 23))) diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityModelRulesTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/communities/newCommunity/NewCommunityModelRulesTest.kt similarity index 98% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityModelRulesTest.kt rename to commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/communities/newCommunity/NewCommunityModelRulesTest.kt index 5183ecd75c..619d0b2d58 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityModelRulesTest.kt +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/communities/newCommunity/NewCommunityModelRulesTest.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.communities.newCommunity +package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.newCommunity import com.vitorpamplona.quartz.nip72ModCommunities.rules.tags.PubkeyRuleTag import org.junit.Assert.assertEquals diff --git a/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/settings/SettingsCatalogFilterTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/settings/SettingsCatalogFilterTest.kt index 10b234ad70..cb2c57b5dc 100644 --- a/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/settings/SettingsCatalogFilterTest.kt +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/settings/SettingsCatalogFilterTest.kt @@ -59,7 +59,7 @@ class SettingsCatalogFilterTest { isDanger: Boolean = false, ) = SettingsEntry( titleRes = titleRes, - icon = SettingsIcon.Painter(0, 0), + icon = SettingsIcon.Painter(Res.drawable.relays, 0), keywordsRes = keywordsRes, isDanger = isDanger, onClick = {}, diff --git a/commonsUI/src/skikoMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/PlatformBackHandler.skiko.kt b/commonsUI/src/skikoMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/PlatformBackHandler.skiko.kt new file mode 100644 index 0000000000..70ba995c8b --- /dev/null +++ b/commonsUI/src/skikoMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/PlatformBackHandler.skiko.kt @@ -0,0 +1,32 @@ +/* + * 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.commons.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.backhandler.BackHandler + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +actual fun PlatformBackHandler( + enabled: Boolean, + onBack: () -> Unit, +) = BackHandler(enabled, onBack) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 13297f858e..45b096ea6b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -197,6 +197,7 @@ jetbrains-compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", jetbrains-compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "jetbrainsCompose" } jetbrains-compose-ui-tooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "jetbrainsCompose" } jetbrains-compose-ui-tooling-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "jetbrainsCompose" } +jetbrains-compose-ui-backhandler = { module = "org.jetbrains.compose.ui:ui-backhandler", version.ref = "jetbrainsCompose" } jetbrains-compose-ui-test-junit4 = { module = "org.jetbrains.compose.ui:ui-test-junit4", version.ref = "jetbrainsCompose" } google-mlkit-genai-proofreading = { group = "com.google.mlkit", name = "genai-proofreading", version.ref = "genaiProofreading" } google-mlkit-genai-prompt = { group = "com.google.mlkit", name = "genai-prompt", version.ref = "genaiPrompt" } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/concurrent/ConcurrentSet.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/concurrent/ConcurrentSet.kt index d00545acf6..25636205b5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/concurrent/ConcurrentSet.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/concurrent/ConcurrentSet.kt @@ -38,6 +38,8 @@ expect class ConcurrentSet() { fun size(): Int + fun clear() + /** A point-in-time copy — safe to iterate or diff against without a lock. */ fun snapshot(): Set } diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/concurrent/ConcurrentSet.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/concurrent/ConcurrentSet.jvmAndroid.kt index 94a0754c11..f0ff0d18c7 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/concurrent/ConcurrentSet.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/concurrent/ConcurrentSet.jvmAndroid.kt @@ -31,5 +31,7 @@ actual class ConcurrentSet { actual fun size(): Int = set.size + actual fun clear() = set.clear() + actual fun snapshot(): Set = HashSet(set) } diff --git a/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/concurrent/ConcurrentSet.native.kt b/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/concurrent/ConcurrentSet.native.kt index 70bf86f133..5055decd93 100644 --- a/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/concurrent/ConcurrentSet.native.kt +++ b/quartz/src/nativeMain/kotlin/com/vitorpamplona/quartz/utils/concurrent/ConcurrentSet.native.kt @@ -42,5 +42,7 @@ actual class ConcurrentSet { actual fun size(): Int = ref.load().size + actual fun clear() = ref.store(HashSet()) + actual fun snapshot(): Set = HashSet(ref.load()) }