mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 11:18:24 +00:00
feat: move the app's string catalog to commons Compose resources [WIP: not yet compiled]
Wave 1 of building the Android app for JVM/desktop: a screen cannot move to
commonsUI while its labels come from amethyst/src/main/res.
Compose resources has no blocking, Context-free accessor - outside composition
getString() is suspend - so the 458 `stringRes(context, R.string.x)` call sites
had to be converted before any key could move. Deferring them was not an option:
only 167 of 1,912 keys are used exclusively at composable call sites, and a key
can only live in one resource system.
No runBlocking bridge, per maintainer decision. By scope:
264 @Composable -> drop the context argument
46 already inside launch { } -> loadStringRes(id)
~80 plain fn, caller in a coroutine -> the function becomes suspend
~68 plain fn, no coroutine in reach -> label hoisted into composition
The notification stack (NotificationCategory, NotificationUtils, CallNotifier,
the four notifiers, RelayPurposeSummary, ConversationShortcuts) went suspend as
one component; its public entry points already were. That rippled into
NotificationChannels.Entry and the notification settings screen, which now
drives them from repeatOnLifecycle + produceState. FlowProgressForegroundService
.render is suspend, so are the POW-mining and Blossom-sync services.
ScheduledPostNotifier went suspend on both implementations (its only driver is
CoroutineWorker.doWork, and ScheduledPostPublisher's callbacks were already
suspend, so desktop is unaffected). PowDuration needed a @Composable pair and a
suspend pair - it is read from three screens and from the mining service.
TimeAgoFormatter became @Composable and lost its context parameter entirely.
Catalog: 1,906 keys moved, 67,275 elements across 57 locale dirs.
amethyst/res 2,051 -> 143 keys; commonsUI 3,016 -> 4,926. Four keys referenced
from AndroidManifest/res-xml (app_name, save, rename, block_hide_user) are
copied rather than moved; two that already existed in commons had their app-side
duplicates dropped. Nine keys using bare %d/%s were made positional across 480
elements - positional form is valid for Android too, so behaviour is unchanged.
tools/strings-migrate/migrate.py: the %% literal was read as a bare conversion
(the second % of `%1$d%% of all` started a fresh match and the space flag let
`% o` look like one), and a bulk @keyfile mode replaces ~110k per-key file
rewrites with one pass per file. Verified byte-identical to the old path on a
40-key sample across all 57 locale dirs.
StringResourceCache.kt keeps only painterRes - drawables are still Android
resources. The R.string overloads and the LruCache behind them are gone with the
last R.string reference; compose-resources caches each locale file itself.
Verified:
- zero R.string / R.plurals references left in Kotlin
- all 6,883 Res.string/Res.plurals references resolve to real catalog keys,
no string-vs-plurals kind mismatches
- .claude/hooks/orphan_strings_check.py passes; all 58 XML files parse
- a per-key per-file reference guard confirms no key was lost (it caught a
real bug: a Context-detector that matched any two-argument call ate the key
in `stringRes(Res.string.x, arg)`, 344 references - the detector now uses an
allowlist of expressions that are genuinely a Context)
NOT verified - this has never compiled. The container's shared egress IP is
rate-limited by Maven Central (~50% of requests answer 429) and Gradle disables
a repository on the first failure, so :amethyst:compileFdroidDebugKotlin could
not be driven to completion. Still needing a compiler: the scope classifications
(a stringRes(id) inside a non-composable lambda and a loadStringRes(id) outside
a coroutine are both compile errors), the 98 Int -> StringResource retypings,
and unused imports/vals left by the hoisting. Next: compile-fix loop,
spotlessApply, then lintFdroidBenchmark for ExtraTranslation.
Rationale and the per-file decisions are in
commons/plans/2026-09-22-strings-to-compose-resources.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXvKXakvup4inNFfAhhr4L
This commit is contained in:
+11
-6
@@ -40,9 +40,14 @@ import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_video_open_in_browser
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_video_playback_failed
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_video_playback_failed_description
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -107,7 +112,7 @@ class PlaybackErrorOverlayFitTest {
|
||||
|
||||
private fun assertButtonUsable() {
|
||||
rule
|
||||
.onNodeWithText(stringRes(targetContext, R.string.error_video_open_in_browser))
|
||||
.onNodeWithText(runBlocking { loadStringRes(Res.string.error_video_open_in_browser) })
|
||||
.assertIsDisplayed()
|
||||
.assertHeightIsAtLeast(ButtonDefaults.MinHeight)
|
||||
}
|
||||
@@ -117,7 +122,7 @@ class PlaybackErrorOverlayFitTest {
|
||||
renderInBox(width = 322.dp, height = 181.dp)
|
||||
assertButtonUsable()
|
||||
rule
|
||||
.onNodeWithText(stringRes(targetContext, R.string.error_video_playback_failed))
|
||||
.onNodeWithText(runBlocking { loadStringRes(Res.string.error_video_playback_failed) })
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@@ -138,7 +143,7 @@ class PlaybackErrorOverlayFitTest {
|
||||
.onNodeWithText(
|
||||
stringRes(
|
||||
targetContext,
|
||||
R.string.error_video_playback_failed_description,
|
||||
Res.string.error_video_playback_failed_description,
|
||||
"ERROR_CODE_PARSING_MANIFEST_MALFORMED",
|
||||
),
|
||||
).assertDoesNotExist()
|
||||
@@ -225,7 +230,7 @@ class PlaybackErrorOverlayFitTest {
|
||||
private fun titleHeight(): Dp {
|
||||
val bounds =
|
||||
rule
|
||||
.onNodeWithText(stringRes(targetContext, R.string.error_video_playback_failed))
|
||||
.onNodeWithText(runBlocking { loadStringRes(Res.string.error_video_playback_failed) })
|
||||
.getUnclippedBoundsInRoot()
|
||||
return bounds.bottom - bounds.top
|
||||
}
|
||||
@@ -233,7 +238,7 @@ class PlaybackErrorOverlayFitTest {
|
||||
private fun buttonHeight(): Dp {
|
||||
val bounds =
|
||||
rule
|
||||
.onNodeWithText(stringRes(targetContext, R.string.error_video_open_in_browser))
|
||||
.onNodeWithText(runBlocking { loadStringRes(Res.string.error_video_open_in_browser) })
|
||||
.getUnclippedBoundsInRoot()
|
||||
return bounds.bottom - bounds.top
|
||||
}
|
||||
|
||||
+2
-2
@@ -57,7 +57,6 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.connectedApps.signers.AppConnectResult
|
||||
import com.vitorpamplona.amethyst.commons.connectedApps.signers.AppSignerPolicy
|
||||
import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp
|
||||
@@ -66,6 +65,7 @@ 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.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.cancel
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_connect_block
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_connect_button
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_connect_how_handle
|
||||
@@ -272,7 +272,7 @@ private fun SignerConnectScreen(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
OutlinedButton(onClick = onCancel, modifier = Modifier.weight(1f)) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
Text(stringResource(Res.string.cancel))
|
||||
}
|
||||
Button(onClick = { onConnect(selected) }, modifier = Modifier.weight(1f)) {
|
||||
Text(stringRes(Res.string.napplet_connect_button))
|
||||
|
||||
+3
-2
@@ -22,8 +22,9 @@ package com.vitorpamplona.amethyst.connectedApps.consent
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.connectedApps.signers.AppConnectResult
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_notif_connect_title
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
@@ -88,7 +89,7 @@ object SignerConnectCoordinator {
|
||||
activityClass = SignerConnectActivity::class.java,
|
||||
extraKey = EXTRA_TOKEN,
|
||||
token = token,
|
||||
titleRes = R.string.nip46_signer_notif_connect_title,
|
||||
titleRes = Res.string.nip46_signer_notif_connect_title,
|
||||
)
|
||||
|
||||
return try {
|
||||
|
||||
+5
-3
@@ -69,7 +69,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.connectedApps.signers.SignerOpGrant
|
||||
import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp
|
||||
import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppIcon
|
||||
@@ -93,7 +92,10 @@ import com.vitorpamplona.amethyst.commons.resources.napplet_signer_deny_op
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_batch_allow
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_batch_deny
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_batch_remember
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_batch_select_all
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_batch_select_none
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_batch_signing_as
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_batch_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_messages_with
|
||||
import com.vitorpamplona.amethyst.service.call.CallSessionBridge
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
@@ -483,7 +485,7 @@ private fun BatchedConsentDialog(
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
pluralStringResource(R.plurals.nip46_signer_batch_title, pending.size, pending.size),
|
||||
pluralStringResource(Res.plurals.nip46_signer_batch_title, pending.size, pending.size),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
account.accountName?.let { name ->
|
||||
@@ -505,7 +507,7 @@ private fun BatchedConsentDialog(
|
||||
) {
|
||||
Text(
|
||||
stringResource(
|
||||
if (selected.size == pending.size) R.string.nip46_signer_batch_select_none else R.string.nip46_signer_batch_select_all,
|
||||
if (selected.size == pending.size) Res.string.nip46_signer_batch_select_none else Res.string.nip46_signer_batch_select_all,
|
||||
),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
|
||||
+3
-2
@@ -22,9 +22,10 @@ package com.vitorpamplona.amethyst.connectedApps.consent
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerOp
|
||||
import com.vitorpamplona.amethyst.commons.connectedApps.signers.SignerOpGrant
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_notif_sign_title
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
@@ -140,7 +141,7 @@ object SignerConsentCoordinator {
|
||||
activityClass = SignerConsentActivity::class.java,
|
||||
extraKey = EXTRA_TOKEN,
|
||||
token = "nip46-signer-consent",
|
||||
titleRes = R.string.nip46_signer_notif_sign_title,
|
||||
titleRes = Res.string.nip46_signer_notif_sign_title,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+11
-7
@@ -28,7 +28,11 @@ import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_notif_channel_desc
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_notif_channel_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_notif_tap
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
|
||||
/**
|
||||
* Surfaces a signer consent/connect [android.app.Activity] from the **background**.
|
||||
@@ -51,17 +55,17 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
||||
object SignerConsentNotifier {
|
||||
private const val CHANNEL_ID = "com.vitorpamplona.amethyst.SIGNER_CONSENT_CHANNEL"
|
||||
|
||||
private fun ensureChannel(context: Context): NotificationChannel {
|
||||
private suspend fun ensureChannel(context: Context): NotificationChannel {
|
||||
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
manager.getNotificationChannel(CHANNEL_ID)?.let { return it }
|
||||
|
||||
val channel =
|
||||
NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
stringRes(context, R.string.nip46_signer_notif_channel_name),
|
||||
loadStringRes(Res.string.nip46_signer_notif_channel_name),
|
||||
NotificationManager.IMPORTANCE_HIGH,
|
||||
).apply {
|
||||
description = stringRes(context, R.string.nip46_signer_notif_channel_desc)
|
||||
description = loadStringRes(Res.string.nip46_signer_notif_channel_desc)
|
||||
}
|
||||
manager.createNotificationChannel(channel)
|
||||
return channel
|
||||
@@ -72,7 +76,7 @@ object SignerConsentNotifier {
|
||||
* [activityClass] carrying [token]. Returns the notification id to pass to [cancel] once the
|
||||
* request resolves.
|
||||
*/
|
||||
fun show(
|
||||
suspend fun show(
|
||||
context: Context,
|
||||
activityClass: Class<*>,
|
||||
extraKey: String,
|
||||
@@ -104,8 +108,8 @@ object SignerConsentNotifier {
|
||||
NotificationCompat
|
||||
.Builder(context, channel.id)
|
||||
.setSmallIcon(R.drawable.amethyst)
|
||||
.setContentTitle(stringRes(context, titleRes))
|
||||
.setContentText(stringRes(context, R.string.nip46_signer_notif_tap))
|
||||
.setContentTitle(loadStringRes(titleRes))
|
||||
.setContentText(loadStringRes(Res.string.nip46_signer_notif_tap))
|
||||
.setContentIntent(pendingIntent)
|
||||
.setFullScreenIntent(pendingIntent, true)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
|
||||
@@ -26,9 +26,10 @@ import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.favorite_app_still_loading
|
||||
import com.vitorpamplona.amethyst.model.ThemeType
|
||||
import com.vitorpamplona.amethyst.napplet.NappletLauncher
|
||||
import com.vitorpamplona.amethyst.napplet.NappletWebViewProfiles
|
||||
@@ -146,7 +147,7 @@ object FavoriteAppLauncher {
|
||||
)
|
||||
else -> {
|
||||
Log.w("FavoriteAppLauncher") { "Favorited app not resolvable yet: $coordinate" }
|
||||
Toast.makeText(context, R.string.favorite_app_still_loading, Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(context, Res.string.favorite_app_still_loading, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
@@ -29,6 +28,9 @@ import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendResult
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendStage
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSender
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapShare
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.bolt12_zap_invalid_receipt
|
||||
import com.vitorpamplona.amethyst.commons.resources.bolt12_zap_paid_no_receipt
|
||||
import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
@@ -250,27 +252,27 @@ class AccountZapActions(
|
||||
is PaySuccessResponse -> {
|
||||
val proof = response.result?.payer_proof
|
||||
if (proof.isNullOrBlank()) {
|
||||
onError(R.string.bolt12_zap_paid_no_receipt, null)
|
||||
onError(Res.string.bolt12_zap_paid_no_receipt, null)
|
||||
} else {
|
||||
val zap = Bolt12ZapBuilder.buildZap(zapSigner, intent, proof, anonymous)
|
||||
if (account.cache.bolt12ZapValidator.validate(zap, verifyEventSignature = false) is Bolt12ZapValidation.Valid) {
|
||||
account.cache.justConsumeMyOwnEvent(zap)
|
||||
account.client.publish(zap, account.broadcaster.computeRelayListToBroadcast(zap))
|
||||
} else {
|
||||
onError(R.string.bolt12_zap_invalid_receipt, null)
|
||||
onError(Res.string.bolt12_zap_invalid_receipt, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is IErrorResponseLike -> onNotPaid(response.nwcErrorCode(), response.errorMessage())
|
||||
|
||||
else -> onError(R.string.bolt12_zap_paid_no_receipt, null)
|
||||
else -> onError(Res.string.bolt12_zap_paid_no_receipt, null)
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Log.w("Account", "BOLT12 zap receipt assembly failed after payment", e)
|
||||
onError(R.string.bolt12_zap_paid_no_receipt, null)
|
||||
onError(Res.string.bolt12_zap_paid_no_receipt, null)
|
||||
} finally {
|
||||
onProcessed()
|
||||
}
|
||||
|
||||
@@ -21,7 +21,35 @@
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.accent_color_blue
|
||||
import com.vitorpamplona.amethyst.commons.resources.accent_color_green
|
||||
import com.vitorpamplona.amethyst.commons.resources.accent_color_orange
|
||||
import com.vitorpamplona.amethyst.commons.resources.accent_color_pink
|
||||
import com.vitorpamplona.amethyst.commons.resources.accent_color_purple
|
||||
import com.vitorpamplona.amethyst.commons.resources.accent_color_red
|
||||
import com.vitorpamplona.amethyst.commons.resources.connectivity_type_always
|
||||
import com.vitorpamplona.amethyst.commons.resources.connectivity_type_never
|
||||
import com.vitorpamplona.amethyst.commons.resources.connectivity_type_unmetered_wifi_only
|
||||
import com.vitorpamplona.amethyst.commons.resources.content_warning_hide_all_sensitive_content_option
|
||||
import com.vitorpamplona.amethyst.commons.resources.content_warning_see_warnings_option
|
||||
import com.vitorpamplona.amethyst.commons.resources.content_warning_show_all_sensitive_content_option
|
||||
import com.vitorpamplona.amethyst.commons.resources.dark
|
||||
import com.vitorpamplona.amethyst.commons.resources.font_family_monospace
|
||||
import com.vitorpamplona.amethyst.commons.resources.font_family_sans_serif
|
||||
import com.vitorpamplona.amethyst.commons.resources.font_family_serif
|
||||
import com.vitorpamplona.amethyst.commons.resources.font_family_system
|
||||
import com.vitorpamplona.amethyst.commons.resources.font_size_huge
|
||||
import com.vitorpamplona.amethyst.commons.resources.font_size_large
|
||||
import com.vitorpamplona.amethyst.commons.resources.font_size_normal
|
||||
import com.vitorpamplona.amethyst.commons.resources.font_size_small
|
||||
import com.vitorpamplona.amethyst.commons.resources.gallery_type_classic
|
||||
import com.vitorpamplona.amethyst.commons.resources.gallery_type_modern
|
||||
import com.vitorpamplona.amethyst.commons.resources.light
|
||||
import com.vitorpamplona.amethyst.commons.resources.system
|
||||
import com.vitorpamplona.amethyst.commons.resources.ui_feature_set_type_complete
|
||||
import com.vitorpamplona.amethyst.commons.resources.ui_feature_set_type_performance
|
||||
import com.vitorpamplona.amethyst.commons.resources.ui_feature_set_type_simplified
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Stable
|
||||
@@ -71,9 +99,9 @@ enum class ThemeType(
|
||||
val screenCode: Int,
|
||||
val resourceId: Int,
|
||||
) {
|
||||
SYSTEM(0, R.string.system),
|
||||
LIGHT(1, R.string.light),
|
||||
DARK(2, R.string.dark),
|
||||
SYSTEM(0, Res.string.system),
|
||||
LIGHT(1, Res.string.light),
|
||||
DARK(2, Res.string.dark),
|
||||
}
|
||||
|
||||
fun parseThemeType(code: Int?): ThemeType =
|
||||
@@ -88,12 +116,12 @@ enum class AccentColorType(
|
||||
val screenCode: Int,
|
||||
val resourceId: Int,
|
||||
) {
|
||||
PURPLE(0, R.string.accent_color_purple),
|
||||
BLUE(1, R.string.accent_color_blue),
|
||||
GREEN(2, R.string.accent_color_green),
|
||||
ORANGE(3, R.string.accent_color_orange),
|
||||
RED(4, R.string.accent_color_red),
|
||||
PINK(5, R.string.accent_color_pink),
|
||||
PURPLE(0, Res.string.accent_color_purple),
|
||||
BLUE(1, Res.string.accent_color_blue),
|
||||
GREEN(2, Res.string.accent_color_green),
|
||||
ORANGE(3, Res.string.accent_color_orange),
|
||||
RED(4, Res.string.accent_color_red),
|
||||
PINK(5, Res.string.accent_color_pink),
|
||||
}
|
||||
|
||||
fun parseAccentColorType(screenCode: Int): AccentColorType =
|
||||
@@ -111,10 +139,10 @@ enum class FontFamilyType(
|
||||
val screenCode: Int,
|
||||
val resourceId: Int,
|
||||
) {
|
||||
SYSTEM(0, R.string.font_family_system),
|
||||
SANS_SERIF(1, R.string.font_family_sans_serif),
|
||||
SERIF(2, R.string.font_family_serif),
|
||||
MONOSPACE(3, R.string.font_family_monospace),
|
||||
SYSTEM(0, Res.string.font_family_system),
|
||||
SANS_SERIF(1, Res.string.font_family_sans_serif),
|
||||
SERIF(2, Res.string.font_family_serif),
|
||||
MONOSPACE(3, Res.string.font_family_monospace),
|
||||
}
|
||||
|
||||
fun parseFontFamilyType(screenCode: Int): FontFamilyType =
|
||||
@@ -131,10 +159,10 @@ enum class FontSizeType(
|
||||
val screenCode: Int,
|
||||
val resourceId: Int,
|
||||
) {
|
||||
SMALL(0.85f, 0, R.string.font_size_small),
|
||||
NORMAL(1.0f, 1, R.string.font_size_normal),
|
||||
LARGE(1.15f, 2, R.string.font_size_large),
|
||||
HUGE(1.3f, 3, R.string.font_size_huge),
|
||||
SMALL(0.85f, 0, Res.string.font_size_small),
|
||||
NORMAL(1.0f, 1, Res.string.font_size_normal),
|
||||
LARGE(1.15f, 2, Res.string.font_size_large),
|
||||
HUGE(1.3f, 3, Res.string.font_size_huge),
|
||||
}
|
||||
|
||||
fun parseFontSizeType(screenCode: Int): FontSizeType =
|
||||
@@ -151,26 +179,26 @@ enum class ConnectivityType(
|
||||
val screenCode: Int,
|
||||
val resourceId: Int,
|
||||
) {
|
||||
ALWAYS(null, 0, R.string.connectivity_type_always),
|
||||
WIFI_ONLY(true, 1, R.string.connectivity_type_unmetered_wifi_only),
|
||||
NEVER(false, 2, R.string.connectivity_type_never),
|
||||
ALWAYS(null, 0, Res.string.connectivity_type_always),
|
||||
WIFI_ONLY(true, 1, Res.string.connectivity_type_unmetered_wifi_only),
|
||||
NEVER(false, 2, Res.string.connectivity_type_never),
|
||||
}
|
||||
|
||||
enum class FeatureSetType(
|
||||
val screenCode: Int,
|
||||
val resourceId: Int,
|
||||
) {
|
||||
COMPLETE(0, R.string.ui_feature_set_type_complete),
|
||||
SIMPLIFIED(1, R.string.ui_feature_set_type_simplified),
|
||||
PERFORMANCE(2, R.string.ui_feature_set_type_performance),
|
||||
COMPLETE(0, Res.string.ui_feature_set_type_complete),
|
||||
SIMPLIFIED(1, Res.string.ui_feature_set_type_simplified),
|
||||
PERFORMANCE(2, Res.string.ui_feature_set_type_performance),
|
||||
}
|
||||
|
||||
enum class ProfileGalleryType(
|
||||
val screenCode: Int,
|
||||
val resourceId: Int,
|
||||
) {
|
||||
CLASSIC(0, R.string.gallery_type_classic),
|
||||
MODERN(1, R.string.gallery_type_modern),
|
||||
CLASSIC(0, Res.string.gallery_type_classic),
|
||||
MODERN(1, Res.string.gallery_type_modern),
|
||||
}
|
||||
|
||||
fun parseConnectivityType(code: Boolean?): ConnectivityType =
|
||||
@@ -209,8 +237,8 @@ enum class BooleanType(
|
||||
val screenCode: Int,
|
||||
val reourceId: Int,
|
||||
) {
|
||||
ALWAYS(null, 0, R.string.connectivity_type_always),
|
||||
NEVER(false, 1, R.string.connectivity_type_never),
|
||||
ALWAYS(null, 0, Res.string.connectivity_type_always),
|
||||
NEVER(false, 1, Res.string.connectivity_type_never),
|
||||
}
|
||||
|
||||
fun parseBooleanType(code: Boolean?): BooleanType =
|
||||
@@ -232,9 +260,9 @@ enum class WarningType(
|
||||
val screenCode: Int,
|
||||
val resourceId: Int,
|
||||
) {
|
||||
WARN(null, 0, R.string.content_warning_see_warnings_option),
|
||||
SHOW(true, 1, R.string.content_warning_show_all_sensitive_content_option),
|
||||
HIDE(false, 2, R.string.content_warning_hide_all_sensitive_content_option),
|
||||
WARN(null, 0, Res.string.content_warning_see_warnings_option),
|
||||
SHOW(true, 1, Res.string.content_warning_show_all_sensitive_content_option),
|
||||
HIDE(false, 2, Res.string.content_warning_hide_all_sensitive_content_option),
|
||||
}
|
||||
|
||||
fun parseWarningType(screenCode: Int): WarningType =
|
||||
|
||||
+9
-6
@@ -21,12 +21,15 @@
|
||||
package com.vitorpamplona.amethyst.model.nip46Signer
|
||||
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46PermissionAuthorizer
|
||||
import com.vitorpamplona.amethyst.commons.connectedApps.signers.AppConnectResult
|
||||
import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerOp
|
||||
import com.vitorpamplona.amethyst.commons.connectedApps.signers.SignerOpGrant
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_allow_always_for
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_decrypt_failed
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_remote_app
|
||||
import com.vitorpamplona.amethyst.connectedApps.consent.SignerConnectCoordinator
|
||||
import com.vitorpamplona.amethyst.connectedApps.consent.SignerConnectInfo
|
||||
import com.vitorpamplona.amethyst.connectedApps.consent.SignerConsentCoordinator
|
||||
@@ -65,7 +68,7 @@ object Nip46ConsentBridge {
|
||||
): AppConnectResult {
|
||||
val context = Amethyst.instance.appContext
|
||||
val meta = request.clientMetadata
|
||||
val title = meta?.name?.ifBlank { null } ?: context.getString(R.string.nip46_signer_remote_app)
|
||||
val title = meta?.name?.ifBlank { null } ?: context.getString(Res.string.nip46_signer_remote_app)
|
||||
val domain = meta?.url?.ifBlank { null } ?: (clientPubKey.take(12) + "…")
|
||||
// The identity being connected to lives in the coordinate; show it as an avatar + name.
|
||||
val face = accountFace(coordinate)
|
||||
@@ -100,7 +103,7 @@ object Nip46ConsentBridge {
|
||||
requestedOps: List<NostrSignerOp>,
|
||||
): AppConnectResult {
|
||||
val context = Amethyst.instance.appContext
|
||||
val title = name?.ifBlank { null } ?: context.getString(R.string.nip46_signer_remote_app)
|
||||
val title = name?.ifBlank { null } ?: context.getString(Res.string.nip46_signer_remote_app)
|
||||
val domain = url?.ifBlank { null } ?: (Nip46PermissionAuthorizer.clientPubKeyOf(coordinate)?.take(12)?.plus("…") ?: "")
|
||||
val face = accountFace(coordinate)
|
||||
val info =
|
||||
@@ -138,7 +141,7 @@ object Nip46ConsentBridge {
|
||||
): SignerOpGrant {
|
||||
val context = Amethyst.instance.appContext
|
||||
val info = runCatching { Amethyst.instance.nip46ClientStore.load(coordinate) }.getOrNull()
|
||||
val title = info?.name?.ifBlank { null } ?: context.getString(R.string.nip46_signer_remote_app)
|
||||
val title = info?.name?.ifBlank { null } ?: context.getString(Res.string.nip46_signer_remote_app)
|
||||
|
||||
val consentInfo =
|
||||
Nip46ConsentInfoBuilder.build(
|
||||
@@ -152,8 +155,8 @@ object Nip46ConsentBridge {
|
||||
strings =
|
||||
Nip46ConsentStrings(
|
||||
opLabel = { it.label(context) },
|
||||
allowAlwaysFor = { context.getString(R.string.nip46_signer_allow_always_for, it) },
|
||||
decryptFailed = context.getString(R.string.nip46_signer_decrypt_failed),
|
||||
allowAlwaysFor = { context.getString(Res.string.nip46_signer_allow_always_for, it) },
|
||||
decryptFailed = context.getString(Res.string.nip46_signer_decrypt_failed),
|
||||
),
|
||||
decrypt = { decryptWithAccountSigner(signer, it) },
|
||||
)
|
||||
|
||||
+45
-23
@@ -21,39 +21,61 @@
|
||||
package com.vitorpamplona.amethyst.napplet
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.napplet.NappletCapability
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_identity
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_identity_desc
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_inc
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_inc_desc
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_keys
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_keys_desc
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_notify
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_notify_desc
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_relay
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_relay_desc
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_resource
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_resource_desc
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_signer
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_signer_desc
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_storage
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_storage_desc
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_theme
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_theme_desc
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_upload
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_upload_desc
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_value
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_value_desc
|
||||
|
||||
/** Localized display name for a capability, shared by the consent dialog and the permissions screen. */
|
||||
@StringRes
|
||||
fun NappletCapability.labelRes(): Int =
|
||||
when (this) {
|
||||
NappletCapability.IDENTITY -> R.string.napplet_cap_identity
|
||||
NappletCapability.KEYS -> R.string.napplet_cap_keys
|
||||
NappletCapability.RELAY -> R.string.napplet_cap_relay
|
||||
NappletCapability.SIGNER -> R.string.napplet_cap_signer
|
||||
NappletCapability.STORAGE -> R.string.napplet_cap_storage
|
||||
NappletCapability.VALUE -> R.string.napplet_cap_value
|
||||
NappletCapability.RESOURCE -> R.string.napplet_cap_resource
|
||||
NappletCapability.UPLOAD -> R.string.napplet_cap_upload
|
||||
NappletCapability.THEME -> R.string.napplet_cap_theme
|
||||
NappletCapability.NOTIFY -> R.string.napplet_cap_notify
|
||||
NappletCapability.INC -> R.string.napplet_cap_inc
|
||||
NappletCapability.IDENTITY -> Res.string.napplet_cap_identity
|
||||
NappletCapability.KEYS -> Res.string.napplet_cap_keys
|
||||
NappletCapability.RELAY -> Res.string.napplet_cap_relay
|
||||
NappletCapability.SIGNER -> Res.string.napplet_cap_signer
|
||||
NappletCapability.STORAGE -> Res.string.napplet_cap_storage
|
||||
NappletCapability.VALUE -> Res.string.napplet_cap_value
|
||||
NappletCapability.RESOURCE -> Res.string.napplet_cap_resource
|
||||
NappletCapability.UPLOAD -> Res.string.napplet_cap_upload
|
||||
NappletCapability.THEME -> Res.string.napplet_cap_theme
|
||||
NappletCapability.NOTIFY -> Res.string.napplet_cap_notify
|
||||
NappletCapability.INC -> Res.string.napplet_cap_inc
|
||||
}
|
||||
|
||||
/** Localized one-line description of what a capability lets a napplet do. */
|
||||
@StringRes
|
||||
fun NappletCapability.descriptionRes(): Int =
|
||||
when (this) {
|
||||
NappletCapability.IDENTITY -> R.string.napplet_cap_identity_desc
|
||||
NappletCapability.KEYS -> R.string.napplet_cap_keys_desc
|
||||
NappletCapability.RELAY -> R.string.napplet_cap_relay_desc
|
||||
NappletCapability.SIGNER -> R.string.napplet_cap_signer_desc
|
||||
NappletCapability.STORAGE -> R.string.napplet_cap_storage_desc
|
||||
NappletCapability.VALUE -> R.string.napplet_cap_value_desc
|
||||
NappletCapability.RESOURCE -> R.string.napplet_cap_resource_desc
|
||||
NappletCapability.UPLOAD -> R.string.napplet_cap_upload_desc
|
||||
NappletCapability.THEME -> R.string.napplet_cap_theme_desc
|
||||
NappletCapability.NOTIFY -> R.string.napplet_cap_notify_desc
|
||||
NappletCapability.INC -> R.string.napplet_cap_inc_desc
|
||||
NappletCapability.IDENTITY -> Res.string.napplet_cap_identity_desc
|
||||
NappletCapability.KEYS -> Res.string.napplet_cap_keys_desc
|
||||
NappletCapability.RELAY -> Res.string.napplet_cap_relay_desc
|
||||
NappletCapability.SIGNER -> Res.string.napplet_cap_signer_desc
|
||||
NappletCapability.STORAGE -> Res.string.napplet_cap_storage_desc
|
||||
NappletCapability.VALUE -> Res.string.napplet_cap_value_desc
|
||||
NappletCapability.RESOURCE -> Res.string.napplet_cap_resource_desc
|
||||
NappletCapability.UPLOAD -> Res.string.napplet_cap_upload_desc
|
||||
NappletCapability.THEME -> Res.string.napplet_cap_theme_desc
|
||||
NappletCapability.NOTIFY -> Res.string.napplet_cap_notify_desc
|
||||
NappletCapability.INC -> Res.string.napplet_cap_inc_desc
|
||||
}
|
||||
|
||||
+83
-49
@@ -21,22 +21,56 @@
|
||||
package com.vitorpamplona.amethyst.napplet
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.PluralsRes
|
||||
import androidx.annotation.StringRes
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.browser.OmniboxInput
|
||||
import com.vitorpamplona.amethyst.commons.napplet.NappletCapability
|
||||
import com.vitorpamplona.amethyst.commons.napplet.NappletIdentity
|
||||
import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletRequest
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_diff_follow_added
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_diff_follow_one
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_diff_follow_removed
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_diff_follows
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_diff_joiner
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_diff_mute_added
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_diff_mute_one
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_diff_mute_removed
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_diff_mutes
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_diff_no_baseline
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_diff_none
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_diff_relay_added
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_diff_relay_removed
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_diff_relays
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_diff_unfollow_one
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_diff_unmute_one
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_effect_deletes
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_effect_tags
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_get_pubkey
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_identity_read
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_nip44_decrypt
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_nip44_encrypt
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_notify
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_pay_amount
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_pay_no_amount
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_publish
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_publish_encrypted
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_publish_preview
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_query
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_resource
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_sign
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_storage
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_consent_upload
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_fallback_title
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadPluralStringRes
|
||||
import com.vitorpamplona.amethyst.favorites.BrowserIconRegistry
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.quartz.lightning.LnInvoiceUtil
|
||||
import com.vitorpamplona.quartz.nip01Core.core.fastForEach
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import org.jetbrains.compose.resources.PluralStringResource
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
|
||||
/**
|
||||
* Turns a pending [NappletRequest] into the human-readable [NappletConsentInfo] the consent dialog
|
||||
@@ -56,7 +90,7 @@ class NappletConsentSummary(
|
||||
capability: NappletCapability,
|
||||
request: NappletRequest,
|
||||
): NappletConsentInfo {
|
||||
val untitled = context.getString(R.string.napplet_fallback_title, identity.authorPubKey.take(8))
|
||||
val untitled = context.getString(Res.string.napplet_fallback_title, identity.authorPubKey.take(8))
|
||||
val (title, iconUrl) =
|
||||
if (identity.authorPubKey == "browser") {
|
||||
val host = OmniboxInput.hostOf(identity.identifier) ?: identity.identifier
|
||||
@@ -135,20 +169,20 @@ class NappletConsentSummary(
|
||||
current = account.kind3FollowList.getFollowListEvent()?.tags,
|
||||
proposed = tags,
|
||||
tagName = "p",
|
||||
template = R.string.napplet_consent_diff_follows,
|
||||
added = R.plurals.napplet_consent_diff_follow_added,
|
||||
removed = R.plurals.napplet_consent_diff_follow_removed,
|
||||
oneAdded = R.string.napplet_consent_diff_follow_one,
|
||||
oneRemoved = R.string.napplet_consent_diff_unfollow_one,
|
||||
template = Res.string.napplet_consent_diff_follows,
|
||||
added = Res.plurals.napplet_consent_diff_follow_added,
|
||||
removed = Res.plurals.napplet_consent_diff_follow_removed,
|
||||
oneAdded = Res.string.napplet_consent_diff_follow_one,
|
||||
oneRemoved = Res.string.napplet_consent_diff_unfollow_one,
|
||||
)
|
||||
AdvertisedRelayListEvent.KIND ->
|
||||
diffOf(
|
||||
current = account.nip65RelayList.getNIP65RelayList()?.tags,
|
||||
proposed = tags,
|
||||
tagName = "r",
|
||||
template = R.string.napplet_consent_diff_relays,
|
||||
added = R.plurals.napplet_consent_diff_relay_added,
|
||||
removed = R.plurals.napplet_consent_diff_relay_removed,
|
||||
template = Res.string.napplet_consent_diff_relays,
|
||||
added = Res.plurals.napplet_consent_diff_relay_added,
|
||||
removed = Res.plurals.napplet_consent_diff_relay_removed,
|
||||
)
|
||||
// Public entries only: a mute list also carries encrypted ones, which are not in `tags`
|
||||
// and so cannot be diffed here.
|
||||
@@ -157,21 +191,21 @@ class NappletConsentSummary(
|
||||
current = account.muteList.getMuteList()?.tags,
|
||||
proposed = tags,
|
||||
tagName = "p",
|
||||
template = R.string.napplet_consent_diff_mutes,
|
||||
added = R.plurals.napplet_consent_diff_mute_added,
|
||||
removed = R.plurals.napplet_consent_diff_mute_removed,
|
||||
oneAdded = R.string.napplet_consent_diff_mute_one,
|
||||
oneRemoved = R.string.napplet_consent_diff_unmute_one,
|
||||
template = Res.string.napplet_consent_diff_mutes,
|
||||
added = Res.plurals.napplet_consent_diff_mute_added,
|
||||
removed = Res.plurals.napplet_consent_diff_mute_removed,
|
||||
oneAdded = Res.string.napplet_consent_diff_mute_one,
|
||||
oneRemoved = Res.string.napplet_consent_diff_unmute_one,
|
||||
)
|
||||
// Deletions have no prior version to compare against — the tags are the whole request.
|
||||
DeletionEvent.KIND ->
|
||||
pluralFor(R.plurals.napplet_consent_effect_deletes, countTag(tags, "e") + countTag(tags, "a"))
|
||||
pluralFor(Res.plurals.napplet_consent_effect_deletes, countTag(tags, "e") + countTag(tags, "a"))
|
||||
?.let { Consequence(it) }
|
||||
// Any other kind: at least tell the user tags exist and can be inspected, so an empty
|
||||
// content preview never reads as "there is nothing else here".
|
||||
else ->
|
||||
if (tags.isNotEmpty()) {
|
||||
pluralFor(R.plurals.napplet_consent_effect_tags, tags.size)?.let { Consequence(it) }
|
||||
pluralFor(Res.plurals.napplet_consent_effect_tags, tags.size)?.let { Consequence(it) }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -183,25 +217,25 @@ class NappletConsentSummary(
|
||||
* that silently drops 130 follows, and only a diff surfaces that. Falls back to the total when
|
||||
* nothing is cached to compare against.
|
||||
*/
|
||||
private fun diffOf(
|
||||
private suspend fun diffOf(
|
||||
current: Array<Array<String>>?,
|
||||
proposed: Array<Array<String>>,
|
||||
tagName: String,
|
||||
@StringRes template: Int,
|
||||
@PluralsRes added: Int,
|
||||
@PluralsRes removed: Int,
|
||||
@StringRes oneAdded: Int? = null,
|
||||
@StringRes oneRemoved: Int? = null,
|
||||
template: StringResource,
|
||||
added: PluralStringResource,
|
||||
removed: PluralStringResource,
|
||||
oneAdded: StringResource? = null,
|
||||
oneRemoved: StringResource? = null,
|
||||
): Consequence {
|
||||
val next = valuesOf(proposed, tagName)
|
||||
val previous =
|
||||
current?.let { valuesOf(it, tagName) }
|
||||
?: return Consequence(pluralStringRes(context, R.plurals.napplet_consent_diff_no_baseline, next.size, next.size))
|
||||
?: return Consequence(loadPluralStringRes(Res.plurals.napplet_consent_diff_no_baseline, next.size, next.size))
|
||||
|
||||
val addedKeys = next.filter { it !in previous }
|
||||
val removedKeys = previous.filter { it !in next }
|
||||
if (addedKeys.isEmpty() && removedKeys.isEmpty()) {
|
||||
return Consequence(context.getString(R.string.napplet_consent_diff_none))
|
||||
return Consequence(context.getString(Res.string.napplet_consent_diff_none))
|
||||
}
|
||||
|
||||
// The overwhelmingly common edit is a single follow/unfollow. Naming and picturing that one
|
||||
@@ -217,7 +251,7 @@ class NappletConsentSummary(
|
||||
val parts = listOfNotNull(pluralFor(added, addedKeys.size), pluralFor(removed, removedKeys.size))
|
||||
val summary =
|
||||
if (parts.size == 2) {
|
||||
context.getString(R.string.napplet_consent_diff_joiner, parts[0], parts[1])
|
||||
context.getString(Res.string.napplet_consent_diff_joiner, parts[0], parts[1])
|
||||
} else {
|
||||
parts.first()
|
||||
}
|
||||
@@ -244,67 +278,67 @@ class NappletConsentSummary(
|
||||
return out
|
||||
}
|
||||
|
||||
private fun pluralFor(
|
||||
private suspend fun pluralFor(
|
||||
resId: Int,
|
||||
count: Int,
|
||||
): String? = if (count <= 0) null else pluralStringRes(context, resId, count, count)
|
||||
): String? = if (count <= 0) null else loadPluralStringRes(resId, count, count)
|
||||
|
||||
private fun countTag(
|
||||
tags: Array<Array<String>>,
|
||||
name: String,
|
||||
): Int = tags.count { it.isNotEmpty() && it[0] == name }
|
||||
|
||||
private fun summaryFor(request: NappletRequest): String =
|
||||
private suspend fun summaryFor(request: NappletRequest): String =
|
||||
when (request) {
|
||||
is NappletRequest.GetPublicKey -> context.getString(R.string.napplet_consent_get_pubkey)
|
||||
is NappletRequest.IdentityRead -> context.getString(R.string.napplet_consent_identity_read)
|
||||
is NappletRequest.GetPublicKey -> context.getString(Res.string.napplet_consent_get_pubkey)
|
||||
is NappletRequest.IdentityRead -> context.getString(Res.string.napplet_consent_identity_read)
|
||||
is NappletRequest.Publish -> {
|
||||
val preview = request.content.take(160).trim()
|
||||
if (preview.isEmpty()) {
|
||||
context.getString(R.string.napplet_consent_publish, request.kind)
|
||||
context.getString(Res.string.napplet_consent_publish, request.kind)
|
||||
} else {
|
||||
context.getString(R.string.napplet_consent_publish_preview, request.kind) + "\n“$preview”"
|
||||
context.getString(Res.string.napplet_consent_publish_preview, request.kind) + "\n“$preview”"
|
||||
}
|
||||
}
|
||||
is NappletRequest.SignEvent -> {
|
||||
val preview = request.content.take(160).trim()
|
||||
if (preview.isEmpty()) {
|
||||
context.getString(R.string.napplet_consent_sign, request.kind)
|
||||
context.getString(Res.string.napplet_consent_sign, request.kind)
|
||||
} else {
|
||||
context.getString(R.string.napplet_consent_sign, request.kind) + "\n“$preview”"
|
||||
context.getString(Res.string.napplet_consent_sign, request.kind) + "\n“$preview”"
|
||||
}
|
||||
}
|
||||
is NappletRequest.PublishEncrypted -> context.getString(R.string.napplet_consent_publish_encrypted)
|
||||
is NappletRequest.PublishEncrypted -> context.getString(Res.string.napplet_consent_publish_encrypted)
|
||||
is NappletRequest.Nip44Encrypt -> {
|
||||
val preview = request.plaintext.take(160).trim()
|
||||
val summary = context.getString(R.string.napplet_consent_nip44_encrypt, counterpartyLabel(request.peer))
|
||||
val summary = context.getString(Res.string.napplet_consent_nip44_encrypt, counterpartyLabel(request.peer))
|
||||
if (preview.isEmpty()) summary else summary + "\n\u201C$preview\u201D"
|
||||
}
|
||||
// The ciphertext is meaningless to show, so name the counterparty instead — that is
|
||||
// the decision the user is actually making ("let this page read messages from X").
|
||||
is NappletRequest.Nip44Decrypt -> context.getString(R.string.napplet_consent_nip44_decrypt, counterpartyLabel(request.peer))
|
||||
is NappletRequest.QueryEvents, is NappletRequest.Subscribe -> context.getString(R.string.napplet_consent_query)
|
||||
is NappletRequest.Nip44Decrypt -> context.getString(Res.string.napplet_consent_nip44_decrypt, counterpartyLabel(request.peer))
|
||||
is NappletRequest.QueryEvents, is NappletRequest.Subscribe -> context.getString(Res.string.napplet_consent_query)
|
||||
is NappletRequest.StorageGet, is NappletRequest.StorageSet, is NappletRequest.StorageRemove, is NappletRequest.StorageKeys ->
|
||||
context.getString(R.string.napplet_consent_storage)
|
||||
context.getString(Res.string.napplet_consent_storage)
|
||||
is NappletRequest.NotifyCreate -> {
|
||||
val preview = request.title.take(80).trim()
|
||||
if (preview.isEmpty()) context.getString(R.string.napplet_consent_notify) else context.getString(R.string.napplet_consent_notify) + "\n“$preview”"
|
||||
if (preview.isEmpty()) context.getString(Res.string.napplet_consent_notify) else context.getString(Res.string.napplet_consent_notify) + "\n“$preview”"
|
||||
}
|
||||
is NappletRequest.NotifyList, is NappletRequest.NotifyDismiss -> context.getString(R.string.napplet_consent_notify)
|
||||
is NappletRequest.NotifyList, is NappletRequest.NotifyDismiss -> context.getString(Res.string.napplet_consent_notify)
|
||||
is NappletRequest.PayInvoice -> {
|
||||
// getAmountInSats returns ZERO (not null, not a throw) for an amountless BOLT11, so a
|
||||
// naive read renders "pay 0 sats" — telling the user a payment is free when the amount
|
||||
// is in fact unspecified and decided by the payee. Treat non-positive as "no amount".
|
||||
val sats = runCatching { LnInvoiceUtil.getAmountInSats(request.invoice).toLong() }.getOrNull()
|
||||
if (sats != null && sats > 0) {
|
||||
pluralStringRes(context, R.plurals.napplet_consent_pay_amount, sats.toInt(), sats)
|
||||
loadPluralStringRes(Res.plurals.napplet_consent_pay_amount, sats.toInt(), sats)
|
||||
} else {
|
||||
context.getString(R.string.napplet_consent_pay_no_amount)
|
||||
context.getString(Res.string.napplet_consent_pay_no_amount)
|
||||
}
|
||||
}
|
||||
NappletRequest.ResourceInfo, is NappletRequest.ResourceBytes, is NappletRequest.ResourceBytesMany ->
|
||||
context.getString(R.string.napplet_consent_resource)
|
||||
is NappletRequest.UploadBlob -> context.getString(R.string.napplet_consent_upload)
|
||||
context.getString(Res.string.napplet_consent_resource)
|
||||
is NappletRequest.UploadBlob -> context.getString(Res.string.napplet_consent_upload)
|
||||
// Resolved in the broker before consent (negotiation / shell-mediated / cosmetic); never shown.
|
||||
is NappletRequest.RegisterAction, is NappletRequest.UnregisterAction, is NappletRequest.ThemeGet -> ""
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
package com.vitorpamplona.amethyst.napplet
|
||||
|
||||
import android.content.Context
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.browser.OmniboxInput
|
||||
import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerOp
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
@@ -30,6 +29,13 @@ import com.vitorpamplona.amethyst.commons.napplet.NappletIdentity
|
||||
import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletRequest
|
||||
import com.vitorpamplona.amethyst.commons.napplet.protocol.counterpartyPubKey
|
||||
import com.vitorpamplona.amethyst.commons.napplet.protocol.toNarrowSignerOp
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_fallback_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_op_decrypt
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_op_decrypt_from
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_op_encrypt
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_op_sign_kind_named
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip46_signer_allow_always_for
|
||||
import com.vitorpamplona.amethyst.connectedApps.consent.SignerConnectInfo
|
||||
import com.vitorpamplona.amethyst.connectedApps.consent.SignerConsentInfo
|
||||
import com.vitorpamplona.amethyst.favorites.BrowserIconRegistry
|
||||
@@ -44,10 +50,10 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
/** Human-readable label for a [NostrSignerOp]. */
|
||||
fun NostrSignerOp.label(context: Context): String =
|
||||
when (this) {
|
||||
is NostrSignerOp.SignKind -> context.getString(R.string.napplet_op_sign_kind_named, kindNameFor(context, kind), kind)
|
||||
NostrSignerOp.Encrypt -> context.getString(R.string.napplet_op_encrypt)
|
||||
NostrSignerOp.Decrypt -> context.getString(R.string.napplet_op_decrypt)
|
||||
is NostrSignerOp.DecryptFrom -> context.getString(R.string.napplet_op_decrypt_from, counterpartyLabel(counterparty))
|
||||
is NostrSignerOp.SignKind -> context.getString(Res.string.napplet_op_sign_kind_named, kindNameFor(context, kind), kind)
|
||||
NostrSignerOp.Encrypt -> context.getString(Res.string.napplet_op_encrypt)
|
||||
NostrSignerOp.Decrypt -> context.getString(Res.string.napplet_op_decrypt)
|
||||
is NostrSignerOp.DecryptFrom -> context.getString(Res.string.napplet_op_decrypt_from, counterpartyLabel(counterparty))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,7 +78,7 @@ fun buildSignerConsentInfo(
|
||||
op: NostrSignerOp,
|
||||
request: NappletRequest,
|
||||
): SignerConsentInfo {
|
||||
val untitled = context.getString(R.string.napplet_fallback_title, identity.authorPubKey.take(8))
|
||||
val untitled = context.getString(Res.string.napplet_fallback_title, identity.authorPubKey.take(8))
|
||||
val (title, iconUrl) =
|
||||
if (identity.authorPubKey == "browser") {
|
||||
val host = OmniboxInput.hostOf(identity.identifier) ?: identity.identifier
|
||||
@@ -156,7 +162,7 @@ fun buildSignerConsentInfo(
|
||||
// are present, so deriving them from one value keeps them from disagreeing.
|
||||
narrowOpLabel =
|
||||
(narrowOp as? NostrSignerOp.DecryptFrom)?.let {
|
||||
context.getString(R.string.nip46_signer_allow_always_for, counterpartyLabel(it.counterparty))
|
||||
context.getString(Res.string.nip46_signer_allow_always_for, counterpartyLabel(it.counterparty))
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -172,7 +178,7 @@ fun buildConnectInfo(
|
||||
identity: NappletIdentity,
|
||||
declared: Set<NappletCapability> = emptySet(),
|
||||
): SignerConnectInfo {
|
||||
val untitled = context.getString(R.string.napplet_fallback_title, identity.authorPubKey.take(8))
|
||||
val untitled = context.getString(Res.string.napplet_fallback_title, identity.authorPubKey.take(8))
|
||||
val (title, iconUrl) =
|
||||
if (identity.authorPubKey == "browser") {
|
||||
val host = OmniboxInput.hostOf(identity.identifier) ?: identity.identifier
|
||||
|
||||
+4
-2
@@ -46,6 +46,8 @@ import com.vitorpamplona.amethyst.commons.napplet.NappletUploadGateway
|
||||
import com.vitorpamplona.amethyst.commons.napplet.NappletUploadResult
|
||||
import com.vitorpamplona.amethyst.commons.napplet.NappletWalletGateway
|
||||
import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.napplet_cap_notify
|
||||
import com.vitorpamplona.amethyst.connectedApps.consent.SignerConnectCoordinator
|
||||
import com.vitorpamplona.amethyst.connectedApps.consent.SignerConsentCoordinator
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
@@ -179,14 +181,14 @@ class AccountNappletGateways(
|
||||
val channel =
|
||||
NotificationChannelCompat
|
||||
.Builder(NOTIFY_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
||||
.setName(context.getString(R.string.napplet_cap_notify))
|
||||
.setName(context.getString(Res.string.napplet_cap_notify))
|
||||
.build()
|
||||
manager.createNotificationChannel(channel)
|
||||
val notification =
|
||||
NotificationCompat
|
||||
.Builder(context, NOTIFY_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.amethyst)
|
||||
.setContentTitle(title.ifBlank { context.getString(R.string.napplet_cap_notify) })
|
||||
.setContentTitle(title.ifBlank { context.getString(Res.string.napplet_cap_notify) })
|
||||
.setContentText(body)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
|
||||
@@ -20,41 +20,70 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_400
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_401
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_402
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_403
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_404
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_405
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_406
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_407
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_408
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_409
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_410
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_411
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_412
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_413
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_414
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_415
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_416
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_417
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_426
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_500
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_501
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_502
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_503
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_504
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_505
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_506
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_507
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_508
|
||||
import com.vitorpamplona.amethyst.commons.resources.http_status_511
|
||||
|
||||
class HttpStatusMessages {
|
||||
companion object {
|
||||
fun resourceIdFor(statusCode: Int = 0): Int? =
|
||||
when (statusCode) {
|
||||
400 -> R.string.http_status_400
|
||||
401 -> R.string.http_status_401
|
||||
402 -> R.string.http_status_402
|
||||
403 -> R.string.http_status_403
|
||||
404 -> R.string.http_status_404
|
||||
405 -> R.string.http_status_405
|
||||
406 -> R.string.http_status_406
|
||||
407 -> R.string.http_status_407
|
||||
408 -> R.string.http_status_408
|
||||
409 -> R.string.http_status_409
|
||||
410 -> R.string.http_status_410
|
||||
411 -> R.string.http_status_411
|
||||
412 -> R.string.http_status_412
|
||||
413 -> R.string.http_status_413
|
||||
414 -> R.string.http_status_414
|
||||
415 -> R.string.http_status_415
|
||||
416 -> R.string.http_status_416
|
||||
417 -> R.string.http_status_417
|
||||
426 -> R.string.http_status_426
|
||||
500 -> R.string.http_status_500
|
||||
501 -> R.string.http_status_501
|
||||
502 -> R.string.http_status_502
|
||||
503 -> R.string.http_status_503
|
||||
504 -> R.string.http_status_504
|
||||
505 -> R.string.http_status_505
|
||||
506 -> R.string.http_status_506
|
||||
507 -> R.string.http_status_507
|
||||
508 -> R.string.http_status_508
|
||||
511 -> R.string.http_status_511
|
||||
400 -> Res.string.http_status_400
|
||||
401 -> Res.string.http_status_401
|
||||
402 -> Res.string.http_status_402
|
||||
403 -> Res.string.http_status_403
|
||||
404 -> Res.string.http_status_404
|
||||
405 -> Res.string.http_status_405
|
||||
406 -> Res.string.http_status_406
|
||||
407 -> Res.string.http_status_407
|
||||
408 -> Res.string.http_status_408
|
||||
409 -> Res.string.http_status_409
|
||||
410 -> Res.string.http_status_410
|
||||
411 -> Res.string.http_status_411
|
||||
412 -> Res.string.http_status_412
|
||||
413 -> Res.string.http_status_413
|
||||
414 -> Res.string.http_status_414
|
||||
415 -> Res.string.http_status_415
|
||||
416 -> Res.string.http_status_416
|
||||
417 -> Res.string.http_status_417
|
||||
426 -> Res.string.http_status_426
|
||||
500 -> Res.string.http_status_500
|
||||
501 -> Res.string.http_status_501
|
||||
502 -> Res.string.http_status_502
|
||||
503 -> Res.string.http_status_503
|
||||
504 -> Res.string.http_status_504
|
||||
505 -> Res.string.http_status_505
|
||||
506 -> Res.string.http_status_506
|
||||
507 -> Res.string.http_status_507
|
||||
508 -> Res.string.http_status_508
|
||||
511 -> Res.string.http_status_511
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,14 +21,21 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import android.content.Context
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.payments.PaymentSource
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.clink_debit_no_response
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_dialog_pay_invoice_error
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_parsing_error_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_unable_to_fetch_invoice
|
||||
import com.vitorpamplona.amethyst.commons.resources.podcast_value_error_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.podcast_value_keysend_requires_nwc
|
||||
import com.vitorpamplona.amethyst.commons.resources.podcast_value_no_recipients
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
import com.vitorpamplona.amethyst.ui.nwc.nwcFailureDetail
|
||||
import com.vitorpamplona.amethyst.ui.nwc.nwcTimeoutMessage
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
@@ -88,8 +95,8 @@ class V4VPaymentHandler(
|
||||
val shares = value.computeShares(totalMilliSats)
|
||||
if (shares.isEmpty()) {
|
||||
onError(
|
||||
stringRes(context, R.string.podcast_value_error_title),
|
||||
stringRes(context, R.string.podcast_value_no_recipients),
|
||||
loadStringRes(Res.string.podcast_value_error_title),
|
||||
loadStringRes(Res.string.podcast_value_no_recipients),
|
||||
)
|
||||
return@withContext
|
||||
}
|
||||
@@ -105,8 +112,8 @@ class V4VPaymentHandler(
|
||||
payNodeSharesViaKeysend(nodeShares, boostagram, context, onError)
|
||||
} else {
|
||||
onError(
|
||||
stringRes(context, R.string.podcast_value_error_title),
|
||||
stringRes(context, R.string.podcast_value_keysend_requires_nwc),
|
||||
loadStringRes(Res.string.podcast_value_error_title),
|
||||
loadStringRes(Res.string.podcast_value_keysend_requires_nwc),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -164,11 +171,11 @@ class V4VPaymentHandler(
|
||||
request = request,
|
||||
onResponse = { response: Response? ->
|
||||
response.nwcFailureDetail(context)?.let { detail ->
|
||||
onError(stringRes(context, R.string.error_dialog_pay_invoice_error), detail)
|
||||
onError(loadStringRes(Res.string.error_dialog_pay_invoice_error), detail)
|
||||
}
|
||||
},
|
||||
onTimeout = {
|
||||
onError(stringRes(context, R.string.error_dialog_pay_invoice_error), nwcTimeoutMessage(context))
|
||||
onError(loadStringRes(Res.string.error_dialog_pay_invoice_error), nwcTimeoutMessage(context))
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -231,8 +238,8 @@ class V4VPaymentHandler(
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onError(
|
||||
stringRes(context, R.string.error_unable_to_fetch_invoice),
|
||||
e.message ?: stringRes(context, R.string.error_parsing_error_message),
|
||||
loadStringRes(Res.string.error_unable_to_fetch_invoice),
|
||||
e.message ?: loadStringRes(Res.string.error_parsing_error_message),
|
||||
)
|
||||
null
|
||||
}
|
||||
@@ -258,11 +265,11 @@ class V4VPaymentHandler(
|
||||
zappedNote = zappedNote,
|
||||
onResponse = { response ->
|
||||
response.nwcFailureDetail(context)?.let { detail ->
|
||||
onError(stringRes(context, R.string.error_dialog_pay_invoice_error), detail)
|
||||
onError(loadStringRes(Res.string.error_dialog_pay_invoice_error), detail)
|
||||
}
|
||||
},
|
||||
onTimeout = {
|
||||
onError(stringRes(context, R.string.error_dialog_pay_invoice_error), nwcTimeoutMessage(context))
|
||||
onError(loadStringRes(Res.string.error_dialog_pay_invoice_error), nwcTimeoutMessage(context))
|
||||
},
|
||||
)
|
||||
done++
|
||||
@@ -276,9 +283,9 @@ class V4VPaymentHandler(
|
||||
val response = ClinkDebitPayer.payInvoice(account, source.wallet.pointer, payable.invoice)
|
||||
if (response?.isOk() != true) {
|
||||
onError(
|
||||
stringRes(context, R.string.error_dialog_pay_invoice_error),
|
||||
loadStringRes(Res.string.error_dialog_pay_invoice_error),
|
||||
response?.failureDetail()
|
||||
?: stringRes(context, R.string.clink_debit_no_response),
|
||||
?: loadStringRes(Res.string.clink_debit_no_response),
|
||||
)
|
||||
}
|
||||
done++
|
||||
|
||||
@@ -22,11 +22,23 @@ package com.vitorpamplona.amethyst.service
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.payments.PaymentSource
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.bolt12_payment_failed
|
||||
import com.vitorpamplona.amethyst.commons.resources.bolt12_zap_error
|
||||
import com.vitorpamplona.amethyst.commons.resources.clink_debit_no_response
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_dialog_pay_invoice_error
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_dialog_zap_error
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_unable_to_fetch_invoice
|
||||
import com.vitorpamplona.amethyst.commons.resources.missing_lud16
|
||||
import com.vitorpamplona.amethyst.commons.resources.unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error
|
||||
import com.vitorpamplona.amethyst.commons.resources.user_does_not_have_a_lightning_address_setup_to_receive_sats
|
||||
import com.vitorpamplona.amethyst.commons.resources.user_x_does_not_have_a_lightning_address_setup_to_receive_sats
|
||||
import com.vitorpamplona.amethyst.commons.resources.wallet_connect_pay_invoice_error_error
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
import com.vitorpamplona.amethyst.ui.nwc.nwcFailureDetail
|
||||
@@ -212,15 +224,15 @@ class ZapPaymentHandler(
|
||||
if (it.user != null) {
|
||||
stringRes(
|
||||
context,
|
||||
R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats,
|
||||
Res.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats,
|
||||
it.user.toBestDisplayName(),
|
||||
)
|
||||
} else {
|
||||
stringRes(context, R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats)
|
||||
loadStringRes(Res.string.user_does_not_have_a_lightning_address_setup_to_receive_sats)
|
||||
}
|
||||
|
||||
onError(
|
||||
stringRes(context, R.string.missing_lud16),
|
||||
loadStringRes(Res.string.missing_lud16),
|
||||
message,
|
||||
it.user,
|
||||
)
|
||||
@@ -432,11 +444,11 @@ class ZapPaymentHandler(
|
||||
onError(
|
||||
stringRes(
|
||||
context,
|
||||
R.string.error_unable_to_fetch_invoice,
|
||||
Res.string.error_unable_to_fetch_invoice,
|
||||
),
|
||||
stringRes(
|
||||
context,
|
||||
R.string.unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error,
|
||||
Res.string.unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error,
|
||||
e.message,
|
||||
),
|
||||
null,
|
||||
@@ -477,15 +489,15 @@ class ZapPaymentHandler(
|
||||
progress.step()
|
||||
response.nwcFailureDetail(context)?.let { detail ->
|
||||
onError(
|
||||
stringRes(context, R.string.error_dialog_pay_invoice_error),
|
||||
stringRes(context, R.string.wallet_connect_pay_invoice_error_error, detail),
|
||||
loadStringRes(Res.string.error_dialog_pay_invoice_error),
|
||||
loadStringRes(Res.string.wallet_connect_pay_invoice_error_error, detail),
|
||||
payable.info.user,
|
||||
)
|
||||
}
|
||||
},
|
||||
onTimeout = {
|
||||
onError(
|
||||
stringRes(context, R.string.error_dialog_pay_invoice_error),
|
||||
loadStringRes(Res.string.error_dialog_pay_invoice_error),
|
||||
nwcTimeoutMessage(context),
|
||||
payable.info.user,
|
||||
)
|
||||
@@ -532,12 +544,12 @@ class ZapPaymentHandler(
|
||||
val progress = PaymentProgress(recipients.size, onProgress)
|
||||
|
||||
mapNotNullAsync(recipients) { recipient: Bolt12Recipient ->
|
||||
fun reportBolt12Error(
|
||||
private suspend fun reportBolt12Error(
|
||||
msgRes: Int,
|
||||
detail: String?,
|
||||
) {
|
||||
val msg = if (detail != null) stringRes(context, msgRes, detail) else stringRes(context, msgRes)
|
||||
onError(stringRes(context, R.string.bolt12_zap_error), msg, recipient.user)
|
||||
val msg = if (detail != null) loadStringRes(msgRes, detail) else loadStringRes(msgRes)
|
||||
onError(loadStringRes(Res.string.bolt12_zap_error), msg, recipient.user)
|
||||
}
|
||||
|
||||
account.zaps.sendBolt12Zap(
|
||||
@@ -575,17 +587,17 @@ class ZapPaymentHandler(
|
||||
// Nothing was paid on either rail. Report it as the lightning failure it
|
||||
// is, rather than letting [sendBolt12Zap]'s catch call it "paid, no receipt".
|
||||
Log.w("ZapPaymentHandler", "BOLT11 fallback failed after a refused BOLT12 offer", e)
|
||||
onError(stringRes(context, R.string.error_dialog_zap_error), e.message ?: e.toString(), recipient.user)
|
||||
onError(loadStringRes(Res.string.error_dialog_zap_error), e.message ?: e.toString(), recipient.user)
|
||||
}
|
||||
} else {
|
||||
// bolt12_payment_failed always formats a detail; a wallet may send neither
|
||||
// message nor a recognised code.
|
||||
reportBolt12Error(R.string.bolt12_payment_failed, detail ?: (code ?: NwcErrorCode.OTHER).name)
|
||||
reportBolt12Error(Res.string.bolt12_payment_failed, detail ?: (code ?: NwcErrorCode.OTHER).name)
|
||||
}
|
||||
},
|
||||
onTimeout = {
|
||||
// No response callback will fire, so account for the settlement step here.
|
||||
reportBolt12Error(R.string.bolt12_payment_failed, nwcTimeoutMessage(context))
|
||||
reportBolt12Error(Res.string.bolt12_payment_failed, nwcTimeoutMessage(context))
|
||||
progress.step()
|
||||
},
|
||||
onProcessed = { progress.step() },
|
||||
@@ -639,9 +651,9 @@ class ZapPaymentHandler(
|
||||
progress.step()
|
||||
if (response?.isOk() != true) {
|
||||
onError(
|
||||
stringRes(context, R.string.error_dialog_pay_invoice_error),
|
||||
loadStringRes(Res.string.error_dialog_pay_invoice_error),
|
||||
response?.failureDetail()
|
||||
?: stringRes(context, R.string.clink_debit_no_response),
|
||||
?: loadStringRes(Res.string.clink_debit_no_response),
|
||||
payable.info.user,
|
||||
)
|
||||
}
|
||||
|
||||
+11
-7
@@ -29,8 +29,12 @@ import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.calendar_reminder_channel_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.calendar_reminder_channel_id
|
||||
import com.vitorpamplona.amethyst.commons.resources.calendar_reminder_channel_name
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
/**
|
||||
* Posts user-visible "starting soon" notifications for NIP-52 appointments the user has RSVP'd
|
||||
@@ -52,7 +56,7 @@ object CalendarReminderNotifier {
|
||||
* calendar detail screen (CalendarTimeSlotEvent / CalendarDateSlotEvent
|
||||
* branches in RouteMaker resolve to Route.CalendarEventDetail).
|
||||
*/
|
||||
fun notifyReminder(
|
||||
suspend fun notifyReminder(
|
||||
context: Context,
|
||||
eventId: String,
|
||||
title: String,
|
||||
@@ -61,7 +65,7 @@ object CalendarReminderNotifier {
|
||||
) {
|
||||
ensureChannel(context)
|
||||
val notId = idFor(eventId)
|
||||
val channelId = stringRes(context, R.string.calendar_reminder_channel_id)
|
||||
val channelId = loadStringRes(Res.string.calendar_reminder_channel_id)
|
||||
val tapIntent =
|
||||
Intent(context, MainActivity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
@@ -100,15 +104,15 @@ object CalendarReminderNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
fun ensureChannel(context: Context) {
|
||||
suspend fun ensureChannel(context: Context) {
|
||||
if (channel != null) return
|
||||
channel =
|
||||
NotificationChannel(
|
||||
stringRes(context, R.string.calendar_reminder_channel_id),
|
||||
stringRes(context, R.string.calendar_reminder_channel_name),
|
||||
loadStringRes(Res.string.calendar_reminder_channel_id),
|
||||
loadStringRes(Res.string.calendar_reminder_channel_name),
|
||||
NotificationManager.IMPORTANCE_DEFAULT,
|
||||
).apply {
|
||||
description = stringRes(context, R.string.calendar_reminder_channel_description)
|
||||
description = loadStringRes(Res.string.calendar_reminder_channel_description)
|
||||
}
|
||||
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
nm.createNotificationChannel(channel!!)
|
||||
|
||||
+6
-4
@@ -27,12 +27,14 @@ import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
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.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.service.resourceusage.UsageKeys
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.appt.tags.RSVPStatusTag
|
||||
import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent
|
||||
@@ -98,12 +100,12 @@ class CalendarReminderWorker(
|
||||
// enters the lead window — the old notification stays valid in the system tray.
|
||||
if (store.wasNotified(eventId, start)) return@forEach
|
||||
|
||||
val title = view.title ?: stringRes(applicationContext, R.string.calendar_reminder_default_title)
|
||||
val title = view.title ?: loadStringRes(Res.string.calendar_reminder_default_title)
|
||||
val minutesAway = ((start - now).coerceAtLeast(0L) / 60L).toInt()
|
||||
val body =
|
||||
pluralStringRes(
|
||||
applicationContext,
|
||||
R.plurals.calendar_reminder_body,
|
||||
Res.plurals.calendar_reminder_body,
|
||||
minutesAway,
|
||||
minutesAway,
|
||||
)
|
||||
|
||||
+11
-5
@@ -37,6 +37,12 @@ import androidx.core.content.ContextCompat
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.nipACWebRtcCalls.CallState
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_hangup
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_ongoing
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_ongoing_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_with
|
||||
import com.vitorpamplona.amethyst.ui.call.CallActivity
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -219,10 +225,10 @@ class CallForegroundService : Service() {
|
||||
val channel =
|
||||
NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
getString(R.string.call_ongoing),
|
||||
getString(Res.string.call_ongoing),
|
||||
NotificationManager.IMPORTANCE_LOW,
|
||||
).apply {
|
||||
description = getString(R.string.call_ongoing_description)
|
||||
description = getString(Res.string.call_ongoing_description)
|
||||
}
|
||||
val notificationManager = getSystemService(NotificationManager::class.java)
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
@@ -252,16 +258,16 @@ class CallForegroundService : Service() {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
)
|
||||
|
||||
val contentText = statusText ?: getString(R.string.call_with, peerName)
|
||||
val contentText = statusText ?: getString(Res.string.call_with, peerName)
|
||||
|
||||
return NotificationCompat
|
||||
.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(getString(R.string.app_name))
|
||||
.setContentTitle(getString(Res.string.app_name))
|
||||
.setContentText(contentText)
|
||||
.setSmallIcon(R.drawable.amethyst)
|
||||
.setOngoing(true)
|
||||
.setContentIntent(openCallIntent)
|
||||
.addAction(R.drawable.ic_call_end, getString(R.string.call_hangup), hangupIntent)
|
||||
.addAction(R.drawable.ic_call_end, getString(Res.string.call_hangup), hangupIntent)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
+14
-8
@@ -33,9 +33,15 @@ import coil3.request.ImageRequest
|
||||
import coil3.request.allowHardware
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_calls_channel_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_calls_channel_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_accept
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_incoming
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_reject
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.service.call.CallNotificationReceiver
|
||||
import com.vitorpamplona.amethyst.ui.call.CallActivity
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -63,16 +69,16 @@ object CallNotifier {
|
||||
const val CALL_CHANNEL_ID = "com.vitorpamplona.amethyst.CALL_CHANNEL"
|
||||
private const val CALL_NOTIFICATION_ID = 0x50000
|
||||
|
||||
fun getOrCreateCallChannel(applicationContext: Context): NotificationChannel {
|
||||
suspend fun getOrCreateCallChannel(applicationContext: Context): NotificationChannel {
|
||||
callChannel?.let { return it }
|
||||
|
||||
val channel =
|
||||
NotificationChannel(
|
||||
CALL_CHANNEL_ID,
|
||||
stringRes(applicationContext, R.string.app_notification_calls_channel_name),
|
||||
loadStringRes(Res.string.app_notification_calls_channel_name),
|
||||
NotificationManager.IMPORTANCE_HIGH,
|
||||
).apply {
|
||||
description = stringRes(applicationContext, R.string.app_notification_calls_channel_description)
|
||||
description = loadStringRes(Res.string.app_notification_calls_channel_description)
|
||||
// Silence the notification sound — CallAudioManager plays the ringtone
|
||||
setSound(null, null)
|
||||
enableVibration(false)
|
||||
@@ -91,7 +97,7 @@ object CallNotifier {
|
||||
* full-screen intent that opens [CallActivity] when the device is
|
||||
* unlocked, and provides Accept / Reject actions.
|
||||
*/
|
||||
fun send(
|
||||
suspend fun send(
|
||||
callerName: String,
|
||||
callerBitmap: Bitmap?,
|
||||
applicationContext: Context,
|
||||
@@ -148,7 +154,7 @@ object CallNotifier {
|
||||
NotificationCompat
|
||||
.Builder(applicationContext, channel.id)
|
||||
.setSmallIcon(R.drawable.amethyst)
|
||||
.setContentTitle(stringRes(applicationContext, R.string.call_incoming))
|
||||
.setContentTitle(loadStringRes(Res.string.call_incoming))
|
||||
.setContentText(callerName)
|
||||
.setLargeIcon(callerBitmap)
|
||||
.setContentIntent(contentPendingIntent)
|
||||
@@ -159,8 +165,8 @@ object CallNotifier {
|
||||
.setOngoing(true)
|
||||
.setTimeoutAfter(60_000)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.addAction(R.drawable.amethyst, stringRes(applicationContext, R.string.call_reject), rejectPendingIntent)
|
||||
.addAction(R.drawable.amethyst, stringRes(applicationContext, R.string.call_accept), acceptPendingIntent)
|
||||
.addAction(R.drawable.amethyst, loadStringRes(Res.string.call_reject), rejectPendingIntent)
|
||||
.addAction(R.drawable.amethyst, loadStringRes(Res.string.call_accept), acceptPendingIntent)
|
||||
|
||||
notificationManager.notify("call", CALL_NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
+12
-7
@@ -21,8 +21,13 @@
|
||||
package com.vitorpamplona.amethyst.service.cashu.melt
|
||||
|
||||
import android.content.Context
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.cashu.melt.MeltResult
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.cashu_failed_redemption
|
||||
import com.vitorpamplona.amethyst.commons.resources.cashu_failed_redemption_explainer_error_msg
|
||||
import com.vitorpamplona.amethyst.commons.resources.cashu_unsafe_mint_url
|
||||
import com.vitorpamplona.amethyst.commons.resources.cashu_unsafe_mint_url_explainer
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip60Cashu.mintApi.CashuMintOperations
|
||||
@@ -85,10 +90,10 @@ class MeltProcessor {
|
||||
val sendable = token.totalAmount - fees
|
||||
if (sendable <= 0) {
|
||||
throw LightningAddressResolver.LightningAddressError(
|
||||
stringRes(context, R.string.cashu_failed_redemption),
|
||||
loadStringRes(Res.string.cashu_failed_redemption),
|
||||
stringRes(
|
||||
context,
|
||||
R.string.cashu_failed_redemption_explainer_error_msg,
|
||||
Res.string.cashu_failed_redemption_explainer_error_msg,
|
||||
"Token value ${token.totalAmount} does not cover fees $fees",
|
||||
),
|
||||
)
|
||||
@@ -121,13 +126,13 @@ class MeltProcessor {
|
||||
// message, not the mint's, so don't dress it up as "the mint said".
|
||||
if (e is MintUrlException) {
|
||||
throw LightningAddressResolver.LightningAddressError(
|
||||
stringRes(context, R.string.cashu_unsafe_mint_url),
|
||||
stringRes(context, R.string.cashu_unsafe_mint_url_explainer, e.message),
|
||||
loadStringRes(Res.string.cashu_unsafe_mint_url),
|
||||
loadStringRes(Res.string.cashu_unsafe_mint_url_explainer, e.message),
|
||||
)
|
||||
}
|
||||
throw LightningAddressResolver.LightningAddressError(
|
||||
stringRes(context, R.string.cashu_failed_redemption),
|
||||
stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, e.message),
|
||||
loadStringRes(Res.string.cashu_failed_redemption),
|
||||
loadStringRes(Res.string.cashu_failed_redemption_explainer_error_msg, e.message),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -34,11 +34,11 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.cancel
|
||||
import com.vitorpamplona.amethyst.commons.resources.crashreport_found
|
||||
import com.vitorpamplona.amethyst.commons.resources.crashreport_found_send
|
||||
import com.vitorpamplona.amethyst.commons.resources.would_you_like_to_send_the_recent_crash_report_to_amethyst_in_a_dm_no_personal_information_will_be_shared
|
||||
@@ -76,7 +76,7 @@ fun DisplayCrashMessages(
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { stackTrace.value = null }) {
|
||||
Text(stringRes(R.string.cancel))
|
||||
Text(stringRes(Res.string.cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
|
||||
+7
-6
@@ -32,6 +32,7 @@ import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
@@ -99,7 +100,7 @@ abstract class FlowProgressForegroundService<T> : Service() {
|
||||
/** Keep the service (and notification) alive while this is true; stop once it goes false. */
|
||||
protected abstract fun isActive(value: T): Boolean
|
||||
|
||||
protected abstract fun render(value: T): Content
|
||||
protected abstract suspend fun render(value: T): Content
|
||||
|
||||
/** Invoked by the cancel action. */
|
||||
protected abstract fun cancelAll()
|
||||
@@ -264,7 +265,7 @@ abstract class FlowProgressForegroundService<T> : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNotification(value: T): Notification {
|
||||
private suspend fun buildNotification(value: T): Notification {
|
||||
val content = render(value)
|
||||
val style =
|
||||
when (val bar = content.bar) {
|
||||
@@ -293,7 +294,7 @@ abstract class FlowProgressForegroundService<T> : Service() {
|
||||
.setContentText(content.text)
|
||||
.setStyle(style)
|
||||
.setContentIntent(tapIntent)
|
||||
.addAction(0, stringRes(this, cancelLabelRes), cancelIntent)
|
||||
.addAction(0, loadStringRes(cancelLabelRes), cancelIntent)
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
||||
@@ -303,17 +304,17 @@ abstract class FlowProgressForegroundService<T> : Service() {
|
||||
val secondaryLabel = secondaryLabelRes
|
||||
val secondaryPending = secondaryIntent
|
||||
if (secondaryLabel != null && secondaryPending != null) {
|
||||
builder.addAction(0, stringRes(this, secondaryLabel), secondaryPending)
|
||||
builder.addAction(0, loadStringRes(secondaryLabel), secondaryPending)
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun ensureChannel() {
|
||||
private suspend fun ensureChannel() {
|
||||
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
if (manager.getNotificationChannel(channelId) != null) return
|
||||
manager.createNotificationChannel(
|
||||
NotificationChannel(channelId, stringRes(this, channelNameRes), NotificationManager.IMPORTANCE_LOW).apply {
|
||||
NotificationChannel(channelId, loadStringRes(channelNameRes), NotificationManager.IMPORTANCE_LOW).apply {
|
||||
description = stringRes(this@FlowProgressForegroundService, channelDescRes)
|
||||
setShowBadge(false)
|
||||
},
|
||||
|
||||
+25
-18
@@ -22,7 +22,14 @@ package com.vitorpamplona.amethyst.service.lnurl
|
||||
|
||||
import android.content.Context
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.callback_url_not_found_in_the_user_s_lightning_address_server_configuration_with_user
|
||||
import com.vitorpamplona.amethyst.commons.resources.could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup
|
||||
import com.vitorpamplona.amethyst.commons.resources.could_not_fetch_invoice_from_details
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup_with_user
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_unable_to_fetch_invoice
|
||||
import com.vitorpamplona.amethyst.commons.resources.incorrect_invoice_amount_sats_from_it_should_have_been
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.service.HttpStatusMessages
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.lightning.LnInvoiceUtil
|
||||
@@ -69,8 +76,8 @@ class LightningAddressResolver {
|
||||
): String {
|
||||
val url =
|
||||
assembleUrl(lnAddress) ?: throw LightningAddressError(
|
||||
stringRes(context, R.string.error_unable_to_fetch_invoice),
|
||||
stringRes(context, R.string.could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup, lnAddress),
|
||||
loadStringRes(Res.string.error_unable_to_fetch_invoice),
|
||||
loadStringRes(Res.string.could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup, lnAddress),
|
||||
)
|
||||
|
||||
val client = okHttpClient(url)
|
||||
@@ -88,7 +95,7 @@ class LightningAddressResolver {
|
||||
response.body.string()
|
||||
} else {
|
||||
throw LightningAddressError(
|
||||
stringRes(context, R.string.error_unable_to_fetch_invoice),
|
||||
loadStringRes(Res.string.error_unable_to_fetch_invoice),
|
||||
stringRes(
|
||||
context,
|
||||
R.string
|
||||
@@ -104,7 +111,7 @@ class LightningAddressResolver {
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
throw LightningAddressError(
|
||||
stringRes(context, R.string.error_unable_to_fetch_invoice),
|
||||
loadStringRes(Res.string.error_unable_to_fetch_invoice),
|
||||
stringRes(
|
||||
context,
|
||||
R.string
|
||||
@@ -151,15 +158,15 @@ class LightningAddressResolver {
|
||||
response.body.string()
|
||||
} else {
|
||||
throw LightningAddressError(
|
||||
stringRes(context, R.string.error_unable_to_fetch_invoice),
|
||||
stringRes(context, R.string.could_not_fetch_invoice_from_details, lnCallback, errorMessage(response, context)),
|
||||
loadStringRes(Res.string.error_unable_to_fetch_invoice),
|
||||
loadStringRes(Res.string.could_not_fetch_invoice_from_details, lnCallback, errorMessage(response, context)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun errorMessage(
|
||||
suspend fun errorMessage(
|
||||
response: Response,
|
||||
context: Context,
|
||||
): String {
|
||||
@@ -196,7 +203,7 @@ class LightningAddressResolver {
|
||||
}
|
||||
|
||||
return errorMessage
|
||||
?: HttpStatusMessages.resourceIdFor(response.code)?.let { stringRes(context, it) }
|
||||
?: HttpStatusMessages.resourceIdFor(response.code)?.let { loadStringRes(it) }
|
||||
?: response.message.ifBlank { null }
|
||||
?: response.code.toString()
|
||||
}
|
||||
@@ -237,10 +244,10 @@ class LightningAddressResolver {
|
||||
} catch (t: Throwable) {
|
||||
if (t is CancellationException) throw t
|
||||
throw LightningAddressError(
|
||||
stringRes(context, R.string.error_unable_to_fetch_invoice),
|
||||
loadStringRes(Res.string.error_unable_to_fetch_invoice),
|
||||
stringRes(
|
||||
context,
|
||||
R.string.error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup_with_user,
|
||||
Res.string.error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup_with_user,
|
||||
lnAddress,
|
||||
),
|
||||
)
|
||||
@@ -250,10 +257,10 @@ class LightningAddressResolver {
|
||||
|
||||
if (callbackUrl == null) {
|
||||
throw LightningAddressError(
|
||||
stringRes(context, R.string.error_unable_to_fetch_invoice),
|
||||
loadStringRes(Res.string.error_unable_to_fetch_invoice),
|
||||
stringRes(
|
||||
context,
|
||||
R.string.callback_url_not_found_in_the_user_s_lightning_address_server_configuration_with_user,
|
||||
Res.string.callback_url_not_found_in_the_user_s_lightning_address_server_configuration_with_user,
|
||||
lnAddress,
|
||||
),
|
||||
)
|
||||
@@ -297,7 +304,7 @@ class LightningAddressResolver {
|
||||
} catch (t: Throwable) {
|
||||
if (t is CancellationException) throw t
|
||||
throw LightningAddressError(
|
||||
stringRes(context, R.string.error_unable_to_fetch_invoice),
|
||||
loadStringRes(Res.string.error_unable_to_fetch_invoice),
|
||||
stringRes(
|
||||
context,
|
||||
R.string
|
||||
@@ -315,7 +322,7 @@ class LightningAddressResolver {
|
||||
|
||||
if (reason != null) {
|
||||
throw LightningAddressError(
|
||||
stringRes(context, R.string.error_unable_to_fetch_invoice),
|
||||
loadStringRes(Res.string.error_unable_to_fetch_invoice),
|
||||
stringRes(
|
||||
context,
|
||||
R.string
|
||||
@@ -326,7 +333,7 @@ class LightningAddressResolver {
|
||||
)
|
||||
} else {
|
||||
throw LightningAddressError(
|
||||
stringRes(context, R.string.error_unable_to_fetch_invoice),
|
||||
loadStringRes(Res.string.error_unable_to_fetch_invoice),
|
||||
stringRes(
|
||||
context,
|
||||
R.string
|
||||
@@ -346,10 +353,10 @@ class LightningAddressResolver {
|
||||
if (invoiceAmount.toLong() != expectedAmountInSats) {
|
||||
onProgress(0.0f)
|
||||
throw LightningAddressError(
|
||||
stringRes(context, R.string.error_unable_to_fetch_invoice),
|
||||
loadStringRes(Res.string.error_unable_to_fetch_invoice),
|
||||
stringRes(
|
||||
context,
|
||||
R.string.incorrect_invoice_amount_sats_from_it_should_have_been,
|
||||
Res.string.incorrect_invoice_amount_sats_from_it_should_have_been,
|
||||
invoiceAmount.toLong().toString(),
|
||||
lnAddress,
|
||||
expectedAmountInSats.toString(),
|
||||
|
||||
+13
-6
@@ -41,6 +41,13 @@ import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.nest_notification_broadcasting
|
||||
import com.vitorpamplona.amethyst.commons.resources.nest_notification_channel
|
||||
import com.vitorpamplona.amethyst.commons.resources.nest_notification_channel_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.nest_notification_listening
|
||||
import com.vitorpamplona.amethyst.commons.resources.nest_notification_stop
|
||||
import com.vitorpamplona.amethyst.commons.resources.nest_notification_text
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.NestAudioFocusBus
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.NestAudioFocusState
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.NestNetworkChangeBus
|
||||
@@ -389,19 +396,19 @@ class NestForegroundService : Service() {
|
||||
|
||||
val title =
|
||||
if (promoted) {
|
||||
getString(R.string.nest_notification_broadcasting)
|
||||
getString(Res.string.nest_notification_broadcasting)
|
||||
} else {
|
||||
getString(R.string.nest_notification_listening)
|
||||
getString(Res.string.nest_notification_listening)
|
||||
}
|
||||
|
||||
return NotificationCompat
|
||||
.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(title)
|
||||
.setContentText(getString(R.string.nest_notification_text))
|
||||
.setContentText(getString(Res.string.nest_notification_text))
|
||||
.setSmallIcon(R.drawable.amethyst)
|
||||
.setOngoing(true)
|
||||
.setContentIntent(openIntent)
|
||||
.addAction(0, getString(R.string.nest_notification_stop), stopIntent)
|
||||
.addAction(0, getString(Res.string.nest_notification_stop), stopIntent)
|
||||
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||
.build()
|
||||
}
|
||||
@@ -412,10 +419,10 @@ class NestForegroundService : Service() {
|
||||
mgr.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
getString(R.string.nest_notification_channel),
|
||||
getString(Res.string.nest_notification_channel),
|
||||
NotificationManager.IMPORTANCE_LOW,
|
||||
).apply {
|
||||
description = getString(R.string.nest_notification_channel_description)
|
||||
description = getString(Res.string.nest_notification_channel_description)
|
||||
setShowBadge(false)
|
||||
},
|
||||
)
|
||||
|
||||
+5
-4
@@ -28,9 +28,10 @@ import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_shortcut_account_removed
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
|
||||
/**
|
||||
@@ -140,7 +141,7 @@ object ConversationShortcuts {
|
||||
* the cache is what keeps a conversation alive after it falls out of the top slots, so
|
||||
* dropping only the dynamic copy would leave it recoverable.
|
||||
*/
|
||||
fun removeForAccount(
|
||||
suspend fun removeForAccount(
|
||||
context: Context,
|
||||
accountNpub: String,
|
||||
) {
|
||||
@@ -170,7 +171,7 @@ object ConversationShortcuts {
|
||||
ShortcutManagerCompat.disableShortcuts(
|
||||
context,
|
||||
mine,
|
||||
stringRes(context, R.string.app_notification_shortcut_account_removed),
|
||||
loadStringRes(Res.string.app_notification_shortcut_account_removed),
|
||||
)
|
||||
ShortcutManagerCompat.removeLongLivedShortcuts(context, mine)
|
||||
} catch (e: Exception) {
|
||||
|
||||
+7
-4
@@ -30,13 +30,16 @@ import coil3.request.ImageRequest
|
||||
import coil3.request.allowHardware
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.isMutedPublicChatMessage
|
||||
import com.vitorpamplona.amethyst.commons.nipACWebRtcCalls.CallManager
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.event.EventFinderQueryState
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderQueryState
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_chess_challenge_accepted
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_chess_your_turn
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_poll_channel_message
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.call.notification.CallNotifier
|
||||
import com.vitorpamplona.amethyst.service.notifications.renderers.ArticleNotification
|
||||
@@ -275,7 +278,7 @@ class EventNotificationConsumer(
|
||||
is VideoVerticalEvent,
|
||||
-> MediaNotification.notify(applicationContext, account, event)
|
||||
|
||||
is PollEvent -> MentionNotification.notify(applicationContext, account, event, titleRes = R.string.app_notification_poll_channel_message)
|
||||
is PollEvent -> MentionNotification.notify(applicationContext, account, event, titleRes = Res.string.app_notification_poll_channel_message)
|
||||
|
||||
is HighlightEvent,
|
||||
is LongTextNoteEvent,
|
||||
@@ -292,8 +295,8 @@ class EventNotificationConsumer(
|
||||
is GitStatusClosedEvent -> CodeNotification.notify(applicationContext, account, event)
|
||||
is GitStatusDraftEvent -> CodeNotification.notify(applicationContext, account, event)
|
||||
|
||||
is LiveChessGameAcceptEvent -> ChessNotification.notify(applicationContext, account, event, R.string.app_notification_chess_challenge_accepted)
|
||||
is LiveChessMoveEvent -> ChessNotification.notify(applicationContext, account, event, R.string.app_notification_chess_your_turn)
|
||||
is LiveChessGameAcceptEvent -> ChessNotification.notify(applicationContext, account, event, Res.string.app_notification_chess_challenge_accepted)
|
||||
is LiveChessMoveEvent -> ChessNotification.notify(applicationContext, account, event, Res.string.app_notification_chess_your_turn)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-6
@@ -27,7 +27,12 @@ import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.Person
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_reply_failed
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_reply_retry
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_reply_sending
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_reply_unconfirmed
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
|
||||
/**
|
||||
@@ -105,7 +110,7 @@ fun NotificationManager.renderReplyState(
|
||||
|
||||
private const val TAG = "InlineReplyFeedback"
|
||||
|
||||
private fun NotificationManager.renderReplyStateOrThrow(
|
||||
private suspend fun NotificationManager.renderReplyStateOrThrow(
|
||||
applicationContext: Context,
|
||||
notId: Int,
|
||||
state: ReplyState,
|
||||
@@ -126,7 +131,7 @@ private fun NotificationManager.renderReplyStateOrThrow(
|
||||
// `null` attributes the message to the MessagingStyle's own user.
|
||||
style?.addMessage(state.text, System.currentTimeMillis(), null as Person?)
|
||||
?: builder.setRemoteInputHistory(arrayOf(state.text))
|
||||
builder.setSubText(stringRes(applicationContext, R.string.app_notification_reply_sending))
|
||||
builder.setSubText(loadStringRes(Res.string.app_notification_reply_sending))
|
||||
}
|
||||
|
||||
ReplyState.Sent -> {
|
||||
@@ -136,14 +141,14 @@ private fun NotificationManager.renderReplyStateOrThrow(
|
||||
|
||||
is ReplyState.Unconfirmed -> {
|
||||
if (style == null) builder.setRemoteInputHistory(arrayOf(state.text))
|
||||
builder.setSubText(stringRes(applicationContext, R.string.app_notification_reply_unconfirmed))
|
||||
builder.setSubText(loadStringRes(Res.string.app_notification_reply_unconfirmed))
|
||||
}
|
||||
|
||||
is ReplyState.Failed -> {
|
||||
if (style == null) builder.setRemoteInputHistory(arrayOf(state.text))
|
||||
builder.setSubText(stringRes(applicationContext, R.string.app_notification_reply_failed))
|
||||
builder.setSubText(loadStringRes(Res.string.app_notification_reply_failed))
|
||||
|
||||
val retryLabel = stringRes(applicationContext, R.string.app_notification_reply_retry)
|
||||
val retryLabel = loadStringRes(Res.string.app_notification_reply_retry)
|
||||
|
||||
// Rebuilt from the posted notification, so its actions come with it — and a bare
|
||||
// clearActions() would take Reply and Mark Read with them, leaving a failed reply
|
||||
|
||||
+121
-66
@@ -25,11 +25,66 @@ import android.app.NotificationChannelGroup
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
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.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_articles_channel_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_articles_channel_id
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_articles_channel_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_articles_summary
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_badges_channel_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_badges_channel_id
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_badges_channel_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_badges_summary
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_chess_channel_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_chess_channel_id
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_chess_channel_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_chess_summary
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channel_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channel_id
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channel_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_summary
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_dms_channel_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_dms_channel_id
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_dms_channel_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_dms_summary
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_group_content
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_group_developer
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_group_games
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_group_messages
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_group_payments
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_group_social
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_media_channel_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_media_channel_id
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_media_channel_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_media_summary
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_mentions_channel_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_mentions_channel_id
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_mentions_channel_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_mentions_summary
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_payments_channel_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_payments_channel_id
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_payments_channel_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_payments_summary
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_reactions_channel_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_reactions_channel_id
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_reactions_channel_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_reactions_summary
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_replies_channel_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_replies_channel_id
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_replies_channel_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_replies_summary
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_reposts_channel_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_reposts_channel_id
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_reposts_channel_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_reposts_summary
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_zaps_channel_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_zaps_channel_id
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_zaps_channel_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_zaps_summary
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
|
||||
/**
|
||||
* Logical grouping of notification channels, shown as a section header in the
|
||||
@@ -39,14 +94,14 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
||||
*/
|
||||
enum class NotifChannelGroup(
|
||||
val id: String,
|
||||
@param:StringRes val nameRes: Int,
|
||||
val nameRes: StringResource,
|
||||
) {
|
||||
MESSAGES("com.vitorpamplona.amethyst.group.messages", R.string.app_notification_group_messages),
|
||||
SOCIAL("com.vitorpamplona.amethyst.group.social", R.string.app_notification_group_social),
|
||||
PAYMENTS("com.vitorpamplona.amethyst.group.payments", R.string.app_notification_group_payments),
|
||||
CONTENT("com.vitorpamplona.amethyst.group.content", R.string.app_notification_group_content),
|
||||
DEVELOPER("com.vitorpamplona.amethyst.group.developer", R.string.app_notification_group_developer),
|
||||
GAMES("com.vitorpamplona.amethyst.group.games", R.string.app_notification_group_games),
|
||||
MESSAGES("com.vitorpamplona.amethyst.group.messages", Res.string.app_notification_group_messages),
|
||||
SOCIAL("com.vitorpamplona.amethyst.group.social", Res.string.app_notification_group_social),
|
||||
PAYMENTS("com.vitorpamplona.amethyst.group.payments", Res.string.app_notification_group_payments),
|
||||
CONTENT("com.vitorpamplona.amethyst.group.content", Res.string.app_notification_group_content),
|
||||
DEVELOPER("com.vitorpamplona.amethyst.group.developer", Res.string.app_notification_group_developer),
|
||||
GAMES("com.vitorpamplona.amethyst.group.games", Res.string.app_notification_group_games),
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,10 +121,10 @@ enum class NotifChannelGroup(
|
||||
* table-driven definition every renderer reads from.
|
||||
*/
|
||||
enum class NotificationCategory(
|
||||
@param:StringRes val channelIdRes: Int,
|
||||
@param:StringRes val channelNameRes: Int,
|
||||
@param:StringRes val channelDescriptionRes: Int,
|
||||
@param:StringRes val summaryTextRes: Int,
|
||||
val channelIdRes: StringResource,
|
||||
val channelNameRes: StringResource,
|
||||
val channelDescriptionRes: StringResource,
|
||||
val summaryTextRes: StringResource,
|
||||
val importance: Int,
|
||||
val color: Int,
|
||||
@param:DrawableRes val smallIcon: Int,
|
||||
@@ -81,10 +136,10 @@ enum class NotificationCategory(
|
||||
val colorized: Boolean = false,
|
||||
) {
|
||||
DIRECT_MESSAGE(
|
||||
channelIdRes = R.string.app_notification_dms_channel_id,
|
||||
channelNameRes = R.string.app_notification_dms_channel_name,
|
||||
channelDescriptionRes = R.string.app_notification_dms_channel_description,
|
||||
summaryTextRes = R.string.app_notification_dms_summary,
|
||||
channelIdRes = Res.string.app_notification_dms_channel_id,
|
||||
channelNameRes = Res.string.app_notification_dms_channel_name,
|
||||
channelDescriptionRes = Res.string.app_notification_dms_channel_description,
|
||||
summaryTextRes = Res.string.app_notification_dms_summary,
|
||||
importance = NotificationManager.IMPORTANCE_HIGH,
|
||||
color = 0xFF2196F3.toInt(), // blue
|
||||
smallIcon = R.drawable.ic_notif_message,
|
||||
@@ -94,10 +149,10 @@ enum class NotificationCategory(
|
||||
summaryId = 0x10000,
|
||||
),
|
||||
REPLY(
|
||||
channelIdRes = R.string.app_notification_replies_channel_id,
|
||||
channelNameRes = R.string.app_notification_replies_channel_name,
|
||||
channelDescriptionRes = R.string.app_notification_replies_channel_description,
|
||||
summaryTextRes = R.string.app_notification_replies_summary,
|
||||
channelIdRes = Res.string.app_notification_replies_channel_id,
|
||||
channelNameRes = Res.string.app_notification_replies_channel_name,
|
||||
channelDescriptionRes = Res.string.app_notification_replies_channel_description,
|
||||
summaryTextRes = Res.string.app_notification_replies_summary,
|
||||
importance = NotificationManager.IMPORTANCE_DEFAULT,
|
||||
color = 0xFF7C4DFF.toInt(), // deep purple
|
||||
smallIcon = R.drawable.ic_notif_reply,
|
||||
@@ -109,10 +164,10 @@ enum class NotificationCategory(
|
||||
summaryId = 0x50000,
|
||||
),
|
||||
MENTION(
|
||||
channelIdRes = R.string.app_notification_mentions_channel_id,
|
||||
channelNameRes = R.string.app_notification_mentions_channel_name,
|
||||
channelDescriptionRes = R.string.app_notification_mentions_channel_description,
|
||||
summaryTextRes = R.string.app_notification_mentions_summary,
|
||||
channelIdRes = Res.string.app_notification_mentions_channel_id,
|
||||
channelNameRes = Res.string.app_notification_mentions_channel_name,
|
||||
channelDescriptionRes = Res.string.app_notification_mentions_channel_description,
|
||||
summaryTextRes = Res.string.app_notification_mentions_summary,
|
||||
importance = NotificationManager.IMPORTANCE_DEFAULT,
|
||||
color = 0xFF9C27B0.toInt(), // purple
|
||||
smallIcon = R.drawable.ic_notif_mention,
|
||||
@@ -122,10 +177,10 @@ enum class NotificationCategory(
|
||||
summaryId = 0x60000,
|
||||
),
|
||||
REACTION(
|
||||
channelIdRes = R.string.app_notification_reactions_channel_id,
|
||||
channelNameRes = R.string.app_notification_reactions_channel_name,
|
||||
channelDescriptionRes = R.string.app_notification_reactions_channel_description,
|
||||
summaryTextRes = R.string.app_notification_reactions_summary,
|
||||
channelIdRes = Res.string.app_notification_reactions_channel_id,
|
||||
channelNameRes = Res.string.app_notification_reactions_channel_name,
|
||||
channelDescriptionRes = Res.string.app_notification_reactions_channel_description,
|
||||
summaryTextRes = Res.string.app_notification_reactions_summary,
|
||||
importance = NotificationManager.IMPORTANCE_LOW,
|
||||
color = 0xFFE91E63.toInt(), // pink / heart
|
||||
smallIcon = R.drawable.ic_notif_reaction,
|
||||
@@ -135,10 +190,10 @@ enum class NotificationCategory(
|
||||
summaryId = 0x40000,
|
||||
),
|
||||
REPOST(
|
||||
channelIdRes = R.string.app_notification_reposts_channel_id,
|
||||
channelNameRes = R.string.app_notification_reposts_channel_name,
|
||||
channelDescriptionRes = R.string.app_notification_reposts_channel_description,
|
||||
summaryTextRes = R.string.app_notification_reposts_summary,
|
||||
channelIdRes = Res.string.app_notification_reposts_channel_id,
|
||||
channelNameRes = Res.string.app_notification_reposts_channel_name,
|
||||
channelDescriptionRes = Res.string.app_notification_reposts_channel_description,
|
||||
summaryTextRes = Res.string.app_notification_reposts_summary,
|
||||
importance = NotificationManager.IMPORTANCE_LOW,
|
||||
color = 0xFF4CAF50.toInt(), // green
|
||||
smallIcon = R.drawable.ic_notif_repost,
|
||||
@@ -148,10 +203,10 @@ enum class NotificationCategory(
|
||||
summaryId = 0x70000,
|
||||
),
|
||||
ZAP(
|
||||
channelIdRes = R.string.app_notification_zaps_channel_id,
|
||||
channelNameRes = R.string.app_notification_zaps_channel_name,
|
||||
channelDescriptionRes = R.string.app_notification_zaps_channel_description,
|
||||
summaryTextRes = R.string.app_notification_zaps_summary,
|
||||
channelIdRes = Res.string.app_notification_zaps_channel_id,
|
||||
channelNameRes = Res.string.app_notification_zaps_channel_name,
|
||||
channelDescriptionRes = Res.string.app_notification_zaps_channel_description,
|
||||
summaryTextRes = Res.string.app_notification_zaps_summary,
|
||||
importance = NotificationManager.IMPORTANCE_DEFAULT,
|
||||
color = 0xFFF7931A.toInt(), // bitcoin orange
|
||||
smallIcon = R.drawable.ic_notif_zap,
|
||||
@@ -162,10 +217,10 @@ enum class NotificationCategory(
|
||||
colorized = true,
|
||||
),
|
||||
MEDIA(
|
||||
channelIdRes = R.string.app_notification_media_channel_id,
|
||||
channelNameRes = R.string.app_notification_media_channel_name,
|
||||
channelDescriptionRes = R.string.app_notification_media_channel_description,
|
||||
summaryTextRes = R.string.app_notification_media_summary,
|
||||
channelIdRes = Res.string.app_notification_media_channel_id,
|
||||
channelNameRes = Res.string.app_notification_media_channel_name,
|
||||
channelDescriptionRes = Res.string.app_notification_media_channel_description,
|
||||
summaryTextRes = Res.string.app_notification_media_summary,
|
||||
importance = NotificationManager.IMPORTANCE_DEFAULT,
|
||||
color = 0xFF00BCD4.toInt(), // cyan
|
||||
smallIcon = R.drawable.ic_notif_media,
|
||||
@@ -175,10 +230,10 @@ enum class NotificationCategory(
|
||||
summaryId = 0x80000,
|
||||
),
|
||||
ARTICLE(
|
||||
channelIdRes = R.string.app_notification_articles_channel_id,
|
||||
channelNameRes = R.string.app_notification_articles_channel_name,
|
||||
channelDescriptionRes = R.string.app_notification_articles_channel_description,
|
||||
summaryTextRes = R.string.app_notification_articles_summary,
|
||||
channelIdRes = Res.string.app_notification_articles_channel_id,
|
||||
channelNameRes = Res.string.app_notification_articles_channel_name,
|
||||
channelDescriptionRes = Res.string.app_notification_articles_channel_description,
|
||||
summaryTextRes = Res.string.app_notification_articles_summary,
|
||||
importance = NotificationManager.IMPORTANCE_DEFAULT,
|
||||
color = 0xFF3F51B5.toInt(), // indigo
|
||||
smallIcon = R.drawable.ic_notif_article,
|
||||
@@ -188,10 +243,10 @@ enum class NotificationCategory(
|
||||
summaryId = 0x90000,
|
||||
),
|
||||
CODE(
|
||||
channelIdRes = R.string.app_notification_code_channel_id,
|
||||
channelNameRes = R.string.app_notification_code_channel_name,
|
||||
channelDescriptionRes = R.string.app_notification_code_channel_description,
|
||||
summaryTextRes = R.string.app_notification_code_summary,
|
||||
channelIdRes = Res.string.app_notification_code_channel_id,
|
||||
channelNameRes = Res.string.app_notification_code_channel_name,
|
||||
channelDescriptionRes = Res.string.app_notification_code_channel_description,
|
||||
summaryTextRes = Res.string.app_notification_code_summary,
|
||||
importance = NotificationManager.IMPORTANCE_DEFAULT,
|
||||
color = 0xFF607D8B.toInt(), // slate
|
||||
smallIcon = R.drawable.ic_notif_code,
|
||||
@@ -201,10 +256,10 @@ enum class NotificationCategory(
|
||||
summaryId = 0xA0000,
|
||||
),
|
||||
BADGE(
|
||||
channelIdRes = R.string.app_notification_badges_channel_id,
|
||||
channelNameRes = R.string.app_notification_badges_channel_name,
|
||||
channelDescriptionRes = R.string.app_notification_badges_channel_description,
|
||||
summaryTextRes = R.string.app_notification_badges_summary,
|
||||
channelIdRes = Res.string.app_notification_badges_channel_id,
|
||||
channelNameRes = Res.string.app_notification_badges_channel_name,
|
||||
channelDescriptionRes = Res.string.app_notification_badges_channel_description,
|
||||
summaryTextRes = Res.string.app_notification_badges_summary,
|
||||
importance = NotificationManager.IMPORTANCE_DEFAULT,
|
||||
color = 0xFFFFC107.toInt(), // amber / gold
|
||||
smallIcon = R.drawable.ic_notif_badge,
|
||||
@@ -214,10 +269,10 @@ enum class NotificationCategory(
|
||||
summaryId = 0xB0000,
|
||||
),
|
||||
CHESS(
|
||||
channelIdRes = R.string.app_notification_chess_channel_id,
|
||||
channelNameRes = R.string.app_notification_chess_channel_name,
|
||||
channelDescriptionRes = R.string.app_notification_chess_channel_description,
|
||||
summaryTextRes = R.string.app_notification_chess_summary,
|
||||
channelIdRes = Res.string.app_notification_chess_channel_id,
|
||||
channelNameRes = Res.string.app_notification_chess_channel_name,
|
||||
channelDescriptionRes = Res.string.app_notification_chess_channel_description,
|
||||
summaryTextRes = Res.string.app_notification_chess_summary,
|
||||
importance = NotificationManager.IMPORTANCE_DEFAULT,
|
||||
color = 0xFF795548.toInt(), // brown
|
||||
smallIcon = R.drawable.ic_notif_chess,
|
||||
@@ -227,10 +282,10 @@ enum class NotificationCategory(
|
||||
summaryId = 0x30000,
|
||||
),
|
||||
PAYMENT_RECEIVED(
|
||||
channelIdRes = R.string.app_notification_payments_channel_id,
|
||||
channelNameRes = R.string.app_notification_payments_channel_name,
|
||||
channelDescriptionRes = R.string.app_notification_payments_channel_description,
|
||||
summaryTextRes = R.string.app_notification_payments_summary,
|
||||
channelIdRes = Res.string.app_notification_payments_channel_id,
|
||||
channelNameRes = Res.string.app_notification_payments_channel_name,
|
||||
channelDescriptionRes = Res.string.app_notification_payments_channel_description,
|
||||
summaryTextRes = Res.string.app_notification_payments_summary,
|
||||
importance = NotificationManager.IMPORTANCE_DEFAULT,
|
||||
color = 0xFF16B979.toInt(), // lightning green — distinct from the gold zap channel
|
||||
smallIcon = R.drawable.ic_notif_zap,
|
||||
@@ -241,7 +296,7 @@ enum class NotificationCategory(
|
||||
),
|
||||
;
|
||||
|
||||
fun channelId(context: Context): String = stringRes(context, channelIdRes)
|
||||
suspend fun channelId(context: Context): String = loadStringRes(channelIdRes)
|
||||
|
||||
@Volatile private var channelEnsured = false
|
||||
|
||||
@@ -252,18 +307,18 @@ enum class NotificationCategory(
|
||||
* successful creation instead of paying two IPCs on every post and re-render.
|
||||
* Returns the channel id to post on.
|
||||
*/
|
||||
fun ensureChannel(context: Context): String {
|
||||
suspend fun ensureChannel(context: Context): String {
|
||||
val id = channelId(context)
|
||||
if (channelEnsured) return id
|
||||
synchronized(this) {
|
||||
if (!channelEnsured) {
|
||||
val nm = context.getSystemService(NotificationManager::class.java)
|
||||
nm.createNotificationChannelGroup(
|
||||
NotificationChannelGroup(channelGroup.id, stringRes(context, channelGroup.nameRes)),
|
||||
NotificationChannelGroup(channelGroup.id, loadStringRes(channelGroup.nameRes)),
|
||||
)
|
||||
val channel =
|
||||
NotificationChannel(id, stringRes(context, channelNameRes), importance).apply {
|
||||
description = stringRes(context, channelDescriptionRes)
|
||||
NotificationChannel(id, loadStringRes(channelNameRes), importance).apply {
|
||||
description = loadStringRes(channelDescriptionRes)
|
||||
group = channelGroup.id
|
||||
}
|
||||
nm.createNotificationChannel(channel)
|
||||
|
||||
+9
-6
@@ -25,9 +25,12 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
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.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_calls_channel_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_scheduled_posts_channel_id
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_scheduled_posts_channel_name
|
||||
import com.vitorpamplona.amethyst.isDebug
|
||||
import com.vitorpamplona.amethyst.service.call.notification.CallNotifier
|
||||
import com.vitorpamplona.amethyst.service.scheduledposts.AndroidScheduledPostNotifier
|
||||
@@ -59,8 +62,8 @@ object NotificationChannels {
|
||||
data class Entry(
|
||||
val nameRes: Int,
|
||||
val icon: MaterialSymbol,
|
||||
val channelId: (Context) -> String,
|
||||
val ensure: (Context) -> Unit,
|
||||
val channelId: suspend (Context) -> String,
|
||||
val ensure: suspend (Context) -> Unit,
|
||||
)
|
||||
|
||||
/** The per-kind content channels, derived from [NotificationCategory], in a
|
||||
@@ -81,13 +84,13 @@ object NotificationChannels {
|
||||
} +
|
||||
listOf(
|
||||
Entry(
|
||||
nameRes = R.string.app_notification_scheduled_posts_channel_name,
|
||||
nameRes = Res.string.app_notification_scheduled_posts_channel_name,
|
||||
icon = MaterialSymbols.Schedule,
|
||||
channelId = { stringRes(it, R.string.app_notification_scheduled_posts_channel_id) },
|
||||
channelId = { stringRes(it, Res.string.app_notification_scheduled_posts_channel_id) },
|
||||
ensure = { AndroidScheduledPostNotifier.ensureChannel(it) },
|
||||
),
|
||||
Entry(
|
||||
nameRes = R.string.app_notification_calls_channel_name,
|
||||
nameRes = Res.string.app_notification_calls_channel_name,
|
||||
icon = MaterialSymbols.Call,
|
||||
channelId = { CallNotifier.CALL_CHANNEL_ID },
|
||||
ensure = { CallNotifier.getOrCreateCallChannel(it) },
|
||||
|
||||
+20
-11
@@ -40,8 +40,17 @@ import androidx.core.net.toUri
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.always_on_notif_channel_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.always_on_notif_channel_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.always_on_notif_connected
|
||||
import com.vitorpamplona.amethyst.commons.resources.always_on_notif_connected_foreground
|
||||
import com.vitorpamplona.amethyst.commons.resources.always_on_notif_connecting
|
||||
import com.vitorpamplona.amethyst.commons.resources.always_on_notif_hide_details
|
||||
import com.vitorpamplona.amethyst.commons.resources.always_on_notif_show_details
|
||||
import com.vitorpamplona.amethyst.commons.resources.always_on_notif_title
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadPluralStringRes
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -354,17 +363,17 @@ class NotificationRelayService : Service() {
|
||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private fun buildNotification(connectedRelays: Int): Notification {
|
||||
private suspend fun buildNotification(connectedRelays: Int): Notification {
|
||||
val contentText =
|
||||
when {
|
||||
connectedRelays <= 0 -> getString(R.string.always_on_notif_connecting)
|
||||
connectedRelays <= 0 -> getString(Res.string.always_on_notif_connecting)
|
||||
// Foreground: the pool also holds the feed/finder outbox relays, so the
|
||||
// count reflects all connections, not just the inbox. Backgrounded, the
|
||||
// feeds tear down and only inbox + DM relays remain.
|
||||
MainActivity.isResumed ->
|
||||
pluralStringRes(this, R.plurals.always_on_notif_connected_foreground, connectedRelays, connectedRelays)
|
||||
loadPluralStringRes(Res.plurals.always_on_notif_connected_foreground, connectedRelays, connectedRelays)
|
||||
else ->
|
||||
pluralStringRes(this, R.plurals.always_on_notif_connected, connectedRelays, connectedRelays)
|
||||
loadPluralStringRes(Res.plurals.always_on_notif_connected, connectedRelays, connectedRelays)
|
||||
}
|
||||
|
||||
// Tapping goes to the screen that answers the question the notification raises — "why is it
|
||||
@@ -421,9 +430,9 @@ class NotificationRelayService : Service() {
|
||||
val detailsLabel =
|
||||
getString(
|
||||
if (detailsExpanded) {
|
||||
R.string.always_on_notif_hide_details
|
||||
Res.string.always_on_notif_hide_details
|
||||
} else {
|
||||
R.string.always_on_notif_show_details
|
||||
Res.string.always_on_notif_show_details
|
||||
},
|
||||
)
|
||||
|
||||
@@ -439,14 +448,14 @@ class NotificationRelayService : Service() {
|
||||
// threshold, so no bundle is formed and nothing gets stapled to it.
|
||||
return NotificationCompat
|
||||
.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(getString(R.string.always_on_notif_title))
|
||||
.setContentTitle(getString(Res.string.always_on_notif_title))
|
||||
.setContentText(contentText)
|
||||
.apply {
|
||||
breakdown?.let {
|
||||
setStyle(
|
||||
NotificationCompat
|
||||
.BigTextStyle()
|
||||
.setBigContentTitle(getString(R.string.always_on_notif_title))
|
||||
.setBigContentTitle(getString(Res.string.always_on_notif_title))
|
||||
.bigText(contentText + "\n\n" + it.joinToString("\n")),
|
||||
)
|
||||
}
|
||||
@@ -464,10 +473,10 @@ class NotificationRelayService : Service() {
|
||||
val channel =
|
||||
NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
getString(R.string.always_on_notif_channel_name),
|
||||
getString(Res.string.always_on_notif_channel_name),
|
||||
NotificationManager.IMPORTANCE_LOW,
|
||||
).apply {
|
||||
description = getString(R.string.always_on_notif_channel_description)
|
||||
description = getString(Res.string.always_on_notif_channel_description)
|
||||
setShowBadge(false)
|
||||
}
|
||||
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
+5
-3
@@ -25,7 +25,9 @@ import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.always_on_notif_tile_subtitle_off
|
||||
import com.vitorpamplona.amethyst.commons.resources.always_on_notif_tile_subtitle_on
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -120,9 +122,9 @@ class NotificationServiceTileService : TileService() {
|
||||
tile.subtitleCompat(
|
||||
getString(
|
||||
if (enabled) {
|
||||
R.string.always_on_notif_tile_subtitle_on
|
||||
Res.string.always_on_notif_tile_subtitle_on
|
||||
} else {
|
||||
R.string.always_on_notif_tile_subtitle_off
|
||||
Res.string.always_on_notif_tile_subtitle_off
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
+20
-15
@@ -46,8 +46,13 @@ import coil3.asDrawable
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.allowHardware
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_mark_read_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_me
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_private_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_reply_label
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -243,7 +248,7 @@ object NotificationUtils {
|
||||
.setSmallIcon(category.smallIcon)
|
||||
.setColor(category.color)
|
||||
.setContentTitle(messageTitle)
|
||||
.setContentText(stringRes(applicationContext, R.string.app_notification_private_message))
|
||||
.setContentText(loadStringRes(Res.string.app_notification_private_message))
|
||||
.setContentIntent(contentPendingIntent)
|
||||
.setPriority(category.priority())
|
||||
.setAutoCancel(true)
|
||||
@@ -330,7 +335,7 @@ object NotificationUtils {
|
||||
val me =
|
||||
Person
|
||||
.Builder()
|
||||
.setName(stringRes(applicationContext, R.string.app_notification_me))
|
||||
.setName(loadStringRes(Res.string.app_notification_me))
|
||||
.apply { accountAvatar?.let { setIcon(IconCompat.createWithBitmap(it)) } }
|
||||
.build()
|
||||
|
||||
@@ -373,7 +378,7 @@ object NotificationUtils {
|
||||
.setSmallIcon(category.smallIcon)
|
||||
.setColor(category.color)
|
||||
.setContentTitle(senderName)
|
||||
.setContentText(stringRes(applicationContext, R.string.app_notification_private_message))
|
||||
.setContentText(loadStringRes(Res.string.app_notification_private_message))
|
||||
.setLargeIcon(avatar)
|
||||
.setContentIntent(contentPendingIntent)
|
||||
.setPriority(category.priority())
|
||||
@@ -469,13 +474,13 @@ object NotificationUtils {
|
||||
)
|
||||
}
|
||||
|
||||
private fun replyRemoteInput(applicationContext: Context): RemoteInput =
|
||||
private suspend fun replyRemoteInput(applicationContext: Context): RemoteInput =
|
||||
RemoteInput
|
||||
.Builder(KEY_REPLY_TEXT)
|
||||
.setLabel(stringRes(applicationContext, R.string.app_notification_reply_label))
|
||||
.setLabel(loadStringRes(Res.string.app_notification_reply_label))
|
||||
.build()
|
||||
|
||||
private fun buildReplyAction(
|
||||
private suspend fun buildReplyAction(
|
||||
applicationContext: Context,
|
||||
notId: Int,
|
||||
intent: Intent,
|
||||
@@ -488,14 +493,14 @@ object NotificationUtils {
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
)
|
||||
return NotificationCompat.Action
|
||||
.Builder(R.drawable.ic_action_reply, stringRes(applicationContext, R.string.app_notification_reply_label), replyPendingIntent)
|
||||
.Builder(R.drawable.ic_action_reply, loadStringRes(Res.string.app_notification_reply_label), replyPendingIntent)
|
||||
.addRemoteInput(replyRemoteInput(applicationContext))
|
||||
.setAllowGeneratedReplies(true)
|
||||
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun dmReplyAction(
|
||||
private suspend fun dmReplyAction(
|
||||
applicationContext: Context,
|
||||
notId: Int,
|
||||
eventId: String,
|
||||
@@ -512,7 +517,7 @@ object NotificationUtils {
|
||||
return buildReplyAction(applicationContext, notId, intent)
|
||||
}
|
||||
|
||||
private fun marmotReplyAction(
|
||||
private suspend fun marmotReplyAction(
|
||||
applicationContext: Context,
|
||||
notId: Int,
|
||||
eventId: String,
|
||||
@@ -531,7 +536,7 @@ object NotificationUtils {
|
||||
return buildReplyAction(applicationContext, notId, intent)
|
||||
}
|
||||
|
||||
private fun publicReplyAction(
|
||||
private suspend fun publicReplyAction(
|
||||
applicationContext: Context,
|
||||
notId: Int,
|
||||
eventId: String,
|
||||
@@ -548,7 +553,7 @@ object NotificationUtils {
|
||||
return buildReplyAction(applicationContext, notId, intent)
|
||||
}
|
||||
|
||||
private fun markReadAction(
|
||||
private suspend fun markReadAction(
|
||||
applicationContext: Context,
|
||||
notId: Int,
|
||||
eventId: String,
|
||||
@@ -567,7 +572,7 @@ object NotificationUtils {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
)
|
||||
return NotificationCompat.Action
|
||||
.Builder(R.drawable.ic_action_mark_read, stringRes(applicationContext, R.string.app_notification_mark_read_label), markReadPendingIntent)
|
||||
.Builder(R.drawable.ic_action_mark_read, loadStringRes(Res.string.app_notification_mark_read_label), markReadPendingIntent)
|
||||
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
|
||||
.build()
|
||||
}
|
||||
@@ -676,7 +681,7 @@ object NotificationUtils {
|
||||
* it alone. It costs nothing visually: the shade hides any group with fewer than
|
||||
* two children and shows the child on its own.
|
||||
*/
|
||||
private fun NotificationManager.sendGroupSummary(
|
||||
private suspend fun NotificationManager.sendGroupSummary(
|
||||
category: NotificationCategory,
|
||||
groupKey: String,
|
||||
summaryId: Int,
|
||||
@@ -703,7 +708,7 @@ object NotificationUtils {
|
||||
.setStyle(
|
||||
NotificationCompat
|
||||
.InboxStyle()
|
||||
.setSummaryText(stringRes(applicationContext, category.summaryTextRes)),
|
||||
.setSummaryText(loadStringRes(category.summaryTextRes)),
|
||||
)
|
||||
|
||||
notify(summaryId, summaryBuilder.build())
|
||||
|
||||
+7
-5
@@ -22,10 +22,12 @@ package com.vitorpamplona.amethyst.service.notifications
|
||||
|
||||
import android.content.Context
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.purposes
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_purpose_browsing
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_purpose_line
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadPluralStringRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.SubPurposeLabels
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
@@ -51,7 +53,7 @@ object RelayPurposeSummary {
|
||||
* Lines for the expanded notification, busiest first. Empty when nothing is attributed yet —
|
||||
* the caller must then fall back to the bare count rather than render an empty section.
|
||||
*/
|
||||
fun lines(ctx: Context): List<String> {
|
||||
suspend fun lines(ctx: Context): List<String> {
|
||||
val client = Amethyst.instance.client
|
||||
val named = mutableMapOf<SubPurpose, MutableSet<NormalizedRelayUrl>>()
|
||||
val browsing = mutableSetOf<NormalizedRelayUrl>()
|
||||
@@ -75,11 +77,11 @@ object RelayPurposeSummary {
|
||||
named.entries
|
||||
.sortedWith(compareByDescending<Map.Entry<SubPurpose, Set<NormalizedRelayUrl>>> { it.value.size }.thenBy { it.key.name })
|
||||
.map { (purpose, relays) ->
|
||||
pluralStringRes(ctx, R.plurals.relay_purpose_line, relays.size, ctx.getString(SubPurposeLabels.labelOf(purpose)), relays.size)
|
||||
loadPluralStringRes(Res.plurals.relay_purpose_line, relays.size, ctx.getString(SubPurposeLabels.labelOf(purpose)), relays.size)
|
||||
}.toMutableList()
|
||||
|
||||
if (browsing.isNotEmpty()) {
|
||||
lines.add(pluralStringRes(ctx, R.plurals.relay_purpose_line, browsing.size, ctx.getString(R.string.relay_purpose_browsing), browsing.size))
|
||||
lines.add(loadPluralStringRes(Res.plurals.relay_purpose_line, browsing.size, ctx.getString(Res.string.relay_purpose_browsing), browsing.size))
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
+7
-5
@@ -21,8 +21,11 @@
|
||||
package com.vitorpamplona.amethyst.service.notifications.renderers
|
||||
|
||||
import android.content.Context
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_articles_channel_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_articles_channel_message_highlight
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationContent
|
||||
@@ -30,7 +33,6 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postStandard
|
||||
import com.vitorpamplona.amethyst.service.notifications.notificationManager
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
|
||||
@@ -56,9 +58,9 @@ object ArticleNotification {
|
||||
val isHighlight = event is HighlightEvent
|
||||
val titleRes =
|
||||
if (isHighlight) {
|
||||
R.string.app_notification_articles_channel_message_highlight
|
||||
Res.string.app_notification_articles_channel_message_highlight
|
||||
} else {
|
||||
R.string.app_notification_articles_channel_message
|
||||
Res.string.app_notification_articles_channel_message
|
||||
}
|
||||
val bodySource = if (event is HighlightEvent) event.quote() else event.content
|
||||
val rendered = NotificationContent.renderNoteText(bodySource)
|
||||
@@ -80,7 +82,7 @@ object ArticleNotification {
|
||||
nm.postStandard(
|
||||
category = NotificationCategory.ARTICLE,
|
||||
id = event.id,
|
||||
messageTitle = stringRes(context, titleRes, author.toBestDisplayName()),
|
||||
messageTitle = loadStringRes(titleRes, author.toBestDisplayName()),
|
||||
messageBody = body.text,
|
||||
time = event.createdAt,
|
||||
pictureUrl = author.profilePicture(),
|
||||
|
||||
+6
-4
@@ -21,15 +21,17 @@
|
||||
package com.vitorpamplona.amethyst.service.notifications.renderers
|
||||
|
||||
import android.content.Context
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_badges_channel_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_badges_channel_message_from
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postStandard
|
||||
import com.vitorpamplona.amethyst.service.notifications.notificationManager
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent
|
||||
|
||||
/**
|
||||
@@ -61,8 +63,8 @@ object BadgeNotification {
|
||||
nm.postStandard(
|
||||
category = NotificationCategory.BADGE,
|
||||
id = event.id,
|
||||
messageTitle = stringRes(context, R.string.app_notification_badges_channel_message),
|
||||
messageBody = stringRes(context, R.string.app_notification_badges_channel_message_from, issuer.toBestDisplayName()),
|
||||
messageTitle = loadStringRes(Res.string.app_notification_badges_channel_message),
|
||||
messageBody = loadStringRes(Res.string.app_notification_badges_channel_message_from, issuer.toBestDisplayName()),
|
||||
time = event.createdAt,
|
||||
pictureUrl = issuer.profilePicture(),
|
||||
uri = uri,
|
||||
|
||||
+4
-3
@@ -21,10 +21,12 @@
|
||||
package com.vitorpamplona.amethyst.service.notifications.renderers
|
||||
|
||||
import android.content.Context
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_new_message
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
|
||||
@@ -32,7 +34,6 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.Conversation
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postConversation
|
||||
import com.vitorpamplona.amethyst.service.notifications.notificationManager
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.buzz.workspace.buzzParticipants
|
||||
import com.vitorpamplona.quartz.buzz.workspace.isBuzzDm
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
@@ -78,7 +79,7 @@ object BuzzDmNotification {
|
||||
val channel = buzzDmChannelForMe(note, account.signer.pubKey) ?: return
|
||||
|
||||
val sender = LocalCache.getOrCreateUser(event.pubKey)
|
||||
val body = event.content.takeIf { it.isNotBlank() } ?: stringRes(context, R.string.app_notification_new_message)
|
||||
val body = event.content.takeIf { it.isNotBlank() } ?: loadStringRes(Res.string.app_notification_new_message)
|
||||
|
||||
val accountNpub = NotificationRoutes.accountNpub(account)
|
||||
// The channel's kind-39000 naddr routes straight to the chatroom via the
|
||||
|
||||
+7
-6
@@ -21,9 +21,10 @@
|
||||
package com.vitorpamplona.amethyst.service.notifications.renderers
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_chess_channel_name
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.isDebug
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
|
||||
@@ -31,8 +32,8 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postStandard
|
||||
import com.vitorpamplona.amethyst.service.notifications.notificationManager
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip64Chess.baseEvent.BaseChessEvent
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
|
||||
/**
|
||||
* Chess game notifications — NIP-64 challenge-accepted and move events. Rendered
|
||||
@@ -44,7 +45,7 @@ object ChessNotification {
|
||||
context: Context,
|
||||
account: Account,
|
||||
event: BaseChessEvent,
|
||||
@StringRes contentRes: Int,
|
||||
contentRes: StringResource,
|
||||
) {
|
||||
// NIP-64 chess is a debug-only feature for now; don't notify in release.
|
||||
if (!isDebug) return
|
||||
@@ -65,8 +66,8 @@ object ChessNotification {
|
||||
nm.postStandard(
|
||||
category = NotificationCategory.CHESS,
|
||||
id = event.id,
|
||||
messageTitle = stringRes(context, R.string.app_notification_chess_channel_name),
|
||||
messageBody = stringRes(context, contentRes, author.toBestDisplayName()),
|
||||
messageTitle = loadStringRes(Res.string.app_notification_chess_channel_name),
|
||||
messageBody = loadStringRes(contentRes, author.toBestDisplayName()),
|
||||
time = event.createdAt,
|
||||
pictureUrl = author.profilePicture(),
|
||||
uri = uri,
|
||||
|
||||
+33
-18
@@ -21,8 +21,24 @@
|
||||
package com.vitorpamplona.amethyst.service.notifications.renderers
|
||||
|
||||
import android.content.Context
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channel_message_issue
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channel_message_patch
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channel_message_pr
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channel_message_pr_update
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channel_message_reply
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channel_message_status_applied
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channel_message_status_applied_issue
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channel_message_status_applied_patch
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channel_message_status_applied_pr
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channel_message_status_closed
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channel_message_status_closed_issue
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channel_message_status_closed_patch
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channel_message_status_closed_pr
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channel_message_status_draft
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_code_channel_message_status_open
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationContent
|
||||
@@ -30,7 +46,6 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postStandard
|
||||
import com.vitorpamplona.amethyst.service.notifications.notificationManager
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.pr.GitPullRequestEvent
|
||||
@@ -61,25 +76,25 @@ object CodeNotification {
|
||||
context: Context,
|
||||
account: Account,
|
||||
event: GitIssueEvent,
|
||||
) = post(context, account, event.id, event.createdAt, event.pubKey, R.string.app_notification_code_channel_message_issue, event.subject() ?: event.content)
|
||||
) = post(context, account, event.id, event.createdAt, event.pubKey, Res.string.app_notification_code_channel_message_issue, event.subject() ?: event.content)
|
||||
|
||||
suspend fun notify(
|
||||
context: Context,
|
||||
account: Account,
|
||||
event: GitPatchEvent,
|
||||
) = post(context, account, event.id, event.createdAt, event.pubKey, R.string.app_notification_code_channel_message_patch, event.subject() ?: event.content)
|
||||
) = post(context, account, event.id, event.createdAt, event.pubKey, Res.string.app_notification_code_channel_message_patch, event.subject() ?: event.content)
|
||||
|
||||
suspend fun notify(
|
||||
context: Context,
|
||||
account: Account,
|
||||
event: GitPullRequestEvent,
|
||||
) = post(context, account, event.id, event.createdAt, event.pubKey, R.string.app_notification_code_channel_message_pr, event.subject() ?: event.content)
|
||||
) = post(context, account, event.id, event.createdAt, event.pubKey, Res.string.app_notification_code_channel_message_pr, event.subject() ?: event.content)
|
||||
|
||||
suspend fun notify(
|
||||
context: Context,
|
||||
account: Account,
|
||||
event: GitPullRequestUpdateEvent,
|
||||
) = post(context, account, event.id, event.createdAt, event.pubKey, R.string.app_notification_code_channel_message_pr_update, event.content)
|
||||
) = post(context, account, event.id, event.createdAt, event.pubKey, Res.string.app_notification_code_channel_message_pr_update, event.content)
|
||||
|
||||
// GitReplyEvent (kind 1622) is deprecated in favour of NIP-22 comments, but
|
||||
// events already on relays still arrive and still have to be rendered.
|
||||
@@ -88,13 +103,13 @@ object CodeNotification {
|
||||
context: Context,
|
||||
account: Account,
|
||||
event: GitReplyEvent,
|
||||
) = post(context, account, event.id, event.createdAt, event.pubKey, R.string.app_notification_code_channel_message_reply, event.content)
|
||||
) = post(context, account, event.id, event.createdAt, event.pubKey, Res.string.app_notification_code_channel_message_reply, event.content)
|
||||
|
||||
suspend fun notify(
|
||||
context: Context,
|
||||
account: Account,
|
||||
event: GitStatusOpenEvent,
|
||||
) = post(context, account, event.id, event.createdAt, event.pubKey, R.string.app_notification_code_channel_message_status_open, event.content)
|
||||
) = post(context, account, event.id, event.createdAt, event.pubKey, Res.string.app_notification_code_channel_message_status_open, event.content)
|
||||
|
||||
suspend fun notify(
|
||||
context: Context,
|
||||
@@ -109,10 +124,10 @@ object CodeNotification {
|
||||
titleRes =
|
||||
titleForStatusOnTarget(
|
||||
event.rootEventId(),
|
||||
pr = R.string.app_notification_code_channel_message_status_applied_pr,
|
||||
patch = R.string.app_notification_code_channel_message_status_applied_patch,
|
||||
issue = R.string.app_notification_code_channel_message_status_applied_issue,
|
||||
fallback = R.string.app_notification_code_channel_message_status_applied,
|
||||
pr = Res.string.app_notification_code_channel_message_status_applied_pr,
|
||||
patch = Res.string.app_notification_code_channel_message_status_applied_patch,
|
||||
issue = Res.string.app_notification_code_channel_message_status_applied_issue,
|
||||
fallback = Res.string.app_notification_code_channel_message_status_applied,
|
||||
),
|
||||
subject = event.content,
|
||||
)
|
||||
@@ -130,10 +145,10 @@ object CodeNotification {
|
||||
titleRes =
|
||||
titleForStatusOnTarget(
|
||||
event.rootEventId(),
|
||||
pr = R.string.app_notification_code_channel_message_status_closed_pr,
|
||||
patch = R.string.app_notification_code_channel_message_status_closed_patch,
|
||||
issue = R.string.app_notification_code_channel_message_status_closed_issue,
|
||||
fallback = R.string.app_notification_code_channel_message_status_closed,
|
||||
pr = Res.string.app_notification_code_channel_message_status_closed_pr,
|
||||
patch = Res.string.app_notification_code_channel_message_status_closed_patch,
|
||||
issue = Res.string.app_notification_code_channel_message_status_closed_issue,
|
||||
fallback = Res.string.app_notification_code_channel_message_status_closed,
|
||||
),
|
||||
subject = event.content,
|
||||
)
|
||||
@@ -142,7 +157,7 @@ object CodeNotification {
|
||||
context: Context,
|
||||
account: Account,
|
||||
event: GitStatusDraftEvent,
|
||||
) = post(context, account, event.id, event.createdAt, event.pubKey, R.string.app_notification_code_channel_message_status_draft, event.content)
|
||||
) = post(context, account, event.id, event.createdAt, event.pubKey, Res.string.app_notification_code_channel_message_status_draft, event.content)
|
||||
|
||||
/**
|
||||
* Pick a title string for a status event based on the *target*'s kind, so
|
||||
@@ -196,7 +211,7 @@ object CodeNotification {
|
||||
nm.postStandard(
|
||||
category = NotificationCategory.CODE,
|
||||
id = id,
|
||||
messageTitle = stringRes(context, titleRes, author.toBestDisplayName()),
|
||||
messageTitle = loadStringRes(titleRes, author.toBestDisplayName()),
|
||||
messageBody = body,
|
||||
time = createdAt,
|
||||
pictureUrl = author.profilePicture(),
|
||||
|
||||
+6
-4
@@ -21,8 +21,11 @@
|
||||
package com.vitorpamplona.amethyst.service.notifications.renderers
|
||||
|
||||
import android.content.Context
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_added_to_group
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_new_message
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
|
||||
@@ -32,7 +35,6 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.ReplyA
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postConversation
|
||||
import com.vitorpamplona.amethyst.service.notifications.notificationManager
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.marmot.mip02Welcome.WelcomeEvent
|
||||
import com.vitorpamplona.quartz.nipC7Chats.ChatEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
@@ -58,7 +60,7 @@ object GroupMessageNotification {
|
||||
val chatroom = account.marmotGroupList.getOrCreateGroup(nostrGroupId)
|
||||
val groupName = chatroom.displayName.value?.takeIf { it.isNotBlank() } ?: DEFAULT_GROUP_NAME
|
||||
val sender = LocalCache.getOrCreateUser(innerEvent.pubKey)
|
||||
val fallbackBody = innerEvent.content.takeIf { it.isNotBlank() } ?: stringRes(context, R.string.app_notification_new_message)
|
||||
val fallbackBody = innerEvent.content.takeIf { it.isNotBlank() } ?: loadStringRes(Res.string.app_notification_new_message)
|
||||
|
||||
val accountNpub = NotificationRoutes.accountNpub(account)
|
||||
val uri = NotificationRoutes.marmotUri(nostrGroupId, accountNpub)
|
||||
@@ -140,7 +142,7 @@ object GroupMessageNotification {
|
||||
id = event.id,
|
||||
senderName = inviter.toBestDisplayName(),
|
||||
pictureUrl = inviter.profilePicture(),
|
||||
messageBody = stringRes(context, R.string.app_notification_added_to_group, groupName),
|
||||
messageBody = loadStringRes(Res.string.app_notification_added_to_group, groupName),
|
||||
time = event.createdAt,
|
||||
uri = uri,
|
||||
applicationContext = context,
|
||||
|
||||
+7
-5
@@ -21,8 +21,11 @@
|
||||
package com.vitorpamplona.amethyst.service.notifications.renderers
|
||||
|
||||
import android.content.Context
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_media_channel_message_photo
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_media_channel_message_video
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationContent
|
||||
@@ -30,7 +33,6 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postStandard
|
||||
import com.vitorpamplona.amethyst.service.notifications.notificationManager
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip71Video.VideoEvent
|
||||
|
||||
@@ -72,14 +74,14 @@ object MediaNotification {
|
||||
val user = author.toBestDisplayName()
|
||||
val titleRes =
|
||||
if (isVideo) {
|
||||
R.string.app_notification_media_channel_message_video
|
||||
Res.string.app_notification_media_channel_message_video
|
||||
} else {
|
||||
R.string.app_notification_media_channel_message_photo
|
||||
Res.string.app_notification_media_channel_message_photo
|
||||
}
|
||||
nm.postStandard(
|
||||
category = NotificationCategory.MEDIA,
|
||||
id = event.id,
|
||||
messageTitle = stringRes(context, titleRes, user),
|
||||
messageTitle = loadStringRes(titleRes, user),
|
||||
messageBody = NotificationContent.resolveMentions(event.content, 140).text,
|
||||
time = event.createdAt,
|
||||
pictureUrl = author.profilePicture(),
|
||||
|
||||
+6
-5
@@ -21,9 +21,10 @@
|
||||
package com.vitorpamplona.amethyst.service.notifications.renderers
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_mentions_channel_message
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationContent
|
||||
@@ -31,8 +32,8 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postStandard
|
||||
import com.vitorpamplona.amethyst.service.notifications.notificationManager
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
|
||||
/**
|
||||
* Text-mention notifications — someone mentioned, quoted, or cited you in a note
|
||||
@@ -49,7 +50,7 @@ object MentionNotification {
|
||||
account: Account,
|
||||
event: Event,
|
||||
category: NotificationCategory = NotificationCategory.MENTION,
|
||||
@StringRes titleRes: Int = R.string.app_notification_mentions_channel_message,
|
||||
titleRes: StringResource = Res.string.app_notification_mentions_channel_message,
|
||||
) {
|
||||
val note = LocalCache.getNoteIfExists(event.id) ?: return
|
||||
if (!account.isAcceptable(note)) return
|
||||
@@ -80,7 +81,7 @@ object MentionNotification {
|
||||
nm.postStandard(
|
||||
category = category,
|
||||
id = event.id,
|
||||
messageTitle = stringRes(context, titleRes, author.toBestDisplayName()),
|
||||
messageTitle = loadStringRes(titleRes, author.toBestDisplayName()),
|
||||
messageBody = body.text,
|
||||
time = event.createdAt,
|
||||
pictureUrl = author.profilePicture(),
|
||||
|
||||
+4
-3
@@ -21,14 +21,15 @@
|
||||
package com.vitorpamplona.amethyst.service.notifications.renderers
|
||||
|
||||
import android.content.Context
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_payments_channel_message
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postStandard
|
||||
import com.vitorpamplona.amethyst.service.notifications.notificationManager
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransaction
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@@ -57,7 +58,7 @@ object NwcPaymentNotifier {
|
||||
val id = tx.payment_hash ?: tx.invoice ?: tx.created_at?.toString() ?: return
|
||||
val time = tx.settled_at ?: tx.created_at ?: TimeUtils.now()
|
||||
|
||||
val title = stringRes(context, R.string.app_notification_payments_channel_message, amount)
|
||||
val title = loadStringRes(Res.string.app_notification_payments_channel_message, amount)
|
||||
val comment = tx.parsedMetadata()?.displayComment() ?: tx.displayDescription()
|
||||
val body = comment ?: title
|
||||
|
||||
|
||||
+6
-4
@@ -21,8 +21,11 @@
|
||||
package com.vitorpamplona.amethyst.service.notifications.renderers
|
||||
|
||||
import android.content.Context
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_reactions_channel_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_reactions_channel_message_for
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationContent
|
||||
@@ -30,7 +33,6 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postStandard
|
||||
import com.vitorpamplona.amethyst.service.notifications.notificationManager
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
|
||||
|
||||
@@ -81,9 +83,9 @@ object ReactionNotification {
|
||||
val reactedContent = NotificationContent.excerpt(reactedNote?.event?.content, 140)
|
||||
val body =
|
||||
if (reactedContent.isNotBlank()) {
|
||||
stringRes(context, R.string.app_notification_reactions_channel_message_for, reactedContent)
|
||||
loadStringRes(Res.string.app_notification_reactions_channel_message_for, reactedContent)
|
||||
} else {
|
||||
stringRes(context, R.string.app_notification_reactions_channel_message, user)
|
||||
loadStringRes(Res.string.app_notification_reactions_channel_message, user)
|
||||
}
|
||||
nm.postStandard(
|
||||
category = NotificationCategory.REACTION,
|
||||
|
||||
+4
-3
@@ -21,9 +21,11 @@
|
||||
package com.vitorpamplona.amethyst.service.notifications.renderers
|
||||
|
||||
import android.content.Context
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_me
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationContent
|
||||
@@ -35,7 +37,6 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postCo
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.replyGroupKeyFor
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.replySummaryIdFor
|
||||
import com.vitorpamplona.amethyst.service.notifications.notificationManager
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
|
||||
/**
|
||||
@@ -95,7 +96,7 @@ object ReplyNotification {
|
||||
ParentMessage(
|
||||
senderName =
|
||||
if (parentIsFromMe || parentAuthor == null) {
|
||||
stringRes(context, R.string.app_notification_me)
|
||||
loadStringRes(Res.string.app_notification_me)
|
||||
} else {
|
||||
parentAuthor.toBestDisplayName()
|
||||
},
|
||||
|
||||
+4
-3
@@ -21,8 +21,10 @@
|
||||
package com.vitorpamplona.amethyst.service.notifications.renderers
|
||||
|
||||
import android.content.Context
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_reposts_channel_message
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationContent
|
||||
@@ -30,7 +32,6 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postStandard
|
||||
import com.vitorpamplona.amethyst.service.notifications.notificationManager
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
|
||||
@@ -79,7 +80,7 @@ object RepostNotification {
|
||||
nm.postStandard(
|
||||
category = NotificationCategory.REPOST,
|
||||
id = id,
|
||||
messageTitle = stringRes(context, R.string.app_notification_reposts_channel_message, booster.toBestDisplayName()),
|
||||
messageTitle = loadStringRes(Res.string.app_notification_reposts_channel_message, booster.toBestDisplayName()),
|
||||
messageBody = NotificationContent.excerpt(boostedNote?.event?.content, 140),
|
||||
time = createdAt,
|
||||
pictureUrl = booster.profilePicture(),
|
||||
|
||||
+17
-12
@@ -21,10 +21,16 @@
|
||||
package com.vitorpamplona.amethyst.service.notifications.renderers
|
||||
|
||||
import android.content.Context
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_nutzap_channel_message_from
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_onchain_channel_message_from
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_zaps_channel_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_zaps_channel_message_for
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_zaps_channel_message_from
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationContent
|
||||
@@ -33,7 +39,6 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postStandard
|
||||
import com.vitorpamplona.amethyst.service.notifications.notificationManager
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent
|
||||
@@ -76,7 +81,7 @@ object ZapNotification {
|
||||
sender = sender,
|
||||
zappedNote = zappedNote,
|
||||
title = { _ -> zapTitle(context, amount, comment) },
|
||||
body = { user, excerpt -> fromLine(context, R.string.app_notification_zaps_channel_message_from, user, excerpt) },
|
||||
body = { user, excerpt -> fromLine(context, Res.string.app_notification_zaps_channel_message_from, user, excerpt) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -96,7 +101,7 @@ object ZapNotification {
|
||||
createdAt = event.createdAt,
|
||||
sender = sender,
|
||||
zappedNote = zappedNote,
|
||||
title = { user -> stringRes(context, R.string.app_notification_nutzap_channel_message_from, user) },
|
||||
title = { user -> loadStringRes(Res.string.app_notification_nutzap_channel_message_from, user) },
|
||||
body = { user, excerpt -> excerpt.ifBlank { user } },
|
||||
)
|
||||
}
|
||||
@@ -120,12 +125,12 @@ object ZapNotification {
|
||||
zappedNote = zappedNote,
|
||||
title = { user ->
|
||||
if (sats != null) {
|
||||
stringRes(context, R.string.app_notification_zaps_channel_message, showAmount(sats.toBigDecimal()))
|
||||
loadStringRes(Res.string.app_notification_zaps_channel_message, showAmount(sats.toBigDecimal()))
|
||||
} else {
|
||||
stringRes(context, R.string.app_notification_onchain_channel_message_from, user)
|
||||
loadStringRes(Res.string.app_notification_onchain_channel_message_from, user)
|
||||
}
|
||||
},
|
||||
body = { user, excerpt -> fromLine(context, R.string.app_notification_onchain_channel_message_from, user, excerpt) },
|
||||
body = { user, excerpt -> fromLine(context, Res.string.app_notification_onchain_channel_message_from, user, excerpt) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -167,24 +172,24 @@ object ZapNotification {
|
||||
}
|
||||
}
|
||||
|
||||
private fun zapTitle(
|
||||
private suspend fun zapTitle(
|
||||
context: Context,
|
||||
amount: String,
|
||||
comment: String?,
|
||||
): String {
|
||||
val base = stringRes(context, R.string.app_notification_zaps_channel_message, amount)
|
||||
val base = loadStringRes(Res.string.app_notification_zaps_channel_message, amount)
|
||||
return if (comment != null) "$base ($comment)" else base
|
||||
}
|
||||
|
||||
private fun fromLine(
|
||||
private suspend fun fromLine(
|
||||
context: Context,
|
||||
fromRes: Int,
|
||||
user: String,
|
||||
excerpt: String,
|
||||
): String {
|
||||
var content = stringRes(context, fromRes, user)
|
||||
var content = loadStringRes(fromRes, user)
|
||||
if (excerpt.isNotBlank()) {
|
||||
content += " " + stringRes(context, R.string.app_notification_zaps_channel_message_for, excerpt)
|
||||
content += " " + loadStringRes(Res.string.app_notification_zaps_channel_message_for, excerpt)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
+7
-4
@@ -43,9 +43,12 @@ import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.error_video_open_in_browser
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_video_playback_failed
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_video_playback_failed_description
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
|
||||
@@ -109,7 +112,7 @@ fun RenderPlaybackError(
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringRes(R.string.error_video_playback_failed),
|
||||
text = stringRes(Res.string.error_video_playback_failed),
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
textAlign = TextAlign.Center,
|
||||
@@ -119,7 +122,7 @@ fun RenderPlaybackError(
|
||||
Spacer(Modifier.height(4.dp))
|
||||
|
||||
Text(
|
||||
text = stringRes(R.string.error_video_playback_failed_description, errorCodeName),
|
||||
text = stringRes(Res.string.error_video_playback_failed_description, errorCodeName),
|
||||
color = Color.White.copy(alpha = 0.85f),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
textAlign = TextAlign.Center,
|
||||
@@ -143,7 +146,7 @@ fun RenderPlaybackError(
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Text(stringRes(R.string.error_video_open_in_browser))
|
||||
Text(stringRes(Res.string.error_video_open_in_browser))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -39,7 +39,9 @@ import androidx.media3.common.Player
|
||||
import androidx.media3.common.TrackSelectionOverride
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.captions_off
|
||||
import com.vitorpamplona.amethyst.commons.resources.captions_unnamed_track
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -130,7 +132,7 @@ internal fun CaptionLanguagePopup(
|
||||
val baseColors = ButtonDefaults.textButtonColors()
|
||||
val contentColor = MaterialTheme.colorScheme.onBackground
|
||||
val colors = remember(baseColors, contentColor) { baseColors.copy(contentColor = contentColor) }
|
||||
val unnamed = stringRes(R.string.captions_unnamed_track)
|
||||
val unnamed = stringRes(Res.string.captions_unnamed_track)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.background),
|
||||
@@ -139,7 +141,7 @@ internal fun CaptionLanguagePopup(
|
||||
) {
|
||||
TextButton(colors = colors, onClick = onSelectOff) {
|
||||
Text(
|
||||
stringRes(R.string.captions_off),
|
||||
stringRes(Res.string.captions_off),
|
||||
fontWeight = if (!captionsEnabled) FontWeight(1000) else FontWeight(400),
|
||||
)
|
||||
}
|
||||
|
||||
+5
-3
@@ -38,9 +38,11 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.mute_button
|
||||
import com.vitorpamplona.amethyst.commons.resources.muted_button
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
|
||||
@@ -123,7 +125,7 @@ fun MuteButton(
|
||||
fun MutedIcon() {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.AutoMirrored.VolumeOff,
|
||||
contentDescription = stringRes(id = R.string.muted_button),
|
||||
contentDescription = stringRes(id = Res.string.muted_button),
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Size30Modifier,
|
||||
)
|
||||
@@ -133,7 +135,7 @@ fun MutedIcon() {
|
||||
fun MuteIcon() {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.AutoMirrored.VolumeUp,
|
||||
contentDescription = stringRes(id = R.string.mute_button),
|
||||
contentDescription = stringRes(id = Res.string.mute_button),
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Size30Modifier,
|
||||
)
|
||||
|
||||
+9
-5
@@ -37,14 +37,18 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_settings_video_quality
|
||||
import com.vitorpamplona.amethyst.commons.resources.download_to_phone
|
||||
import com.vitorpamplona.amethyst.commons.resources.more_options
|
||||
import com.vitorpamplona.amethyst.commons.resources.mute_button
|
||||
import com.vitorpamplona.amethyst.commons.resources.muted_button
|
||||
import com.vitorpamplona.amethyst.commons.resources.picture_in_picture
|
||||
import com.vitorpamplona.amethyst.commons.resources.playback_actions_dialog_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.share_or_save
|
||||
import com.vitorpamplona.amethyst.commons.resources.video_player_settings_action_fullscreen
|
||||
import com.vitorpamplona.amethyst.model.VideoPlayerAction
|
||||
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||
@@ -165,7 +169,7 @@ fun OverflowMenuButton(
|
||||
) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.MoreVert,
|
||||
contentDescription = stringRes(R.string.more_options),
|
||||
contentDescription = stringRes(Res.string.more_options),
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Size20Modifier,
|
||||
)
|
||||
@@ -195,7 +199,7 @@ fun OverflowMenuButton(
|
||||
VideoPlayerAction.Mute -> {
|
||||
M3ActionRow(
|
||||
icon = if (startingMuteState) MaterialSymbols.AutoMirrored.VolumeOff else MaterialSymbols.AutoMirrored.VolumeUp,
|
||||
text = if (startingMuteState) stringRes(R.string.muted_button) else stringRes(R.string.mute_button),
|
||||
text = if (startingMuteState) stringRes(Res.string.muted_button) else stringRes(Res.string.mute_button),
|
||||
) {
|
||||
menuExpanded.value = false
|
||||
onMuteClick()
|
||||
@@ -215,7 +219,7 @@ fun OverflowMenuButton(
|
||||
VideoPlayerAction.Share -> {
|
||||
M3ActionRow(
|
||||
icon = MaterialSymbols.Share,
|
||||
text = stringRes(R.string.share_or_save),
|
||||
text = stringRes(Res.string.share_or_save),
|
||||
) {
|
||||
menuExpanded.value = false
|
||||
onShareClick()
|
||||
@@ -225,7 +229,7 @@ fun OverflowMenuButton(
|
||||
VideoPlayerAction.Download -> {
|
||||
M3ActionRow(
|
||||
icon = MaterialSymbols.SaveAlt,
|
||||
text = stringRes(R.string.download_to_phone),
|
||||
text = stringRes(Res.string.download_to_phone),
|
||||
) {
|
||||
menuExpanded.value = false
|
||||
onSaveClick()
|
||||
|
||||
+5
-3
@@ -39,9 +39,11 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.pause
|
||||
import com.vitorpamplona.amethyst.commons.resources.play
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.PlayIconSize
|
||||
@@ -117,13 +119,13 @@ fun PlayPauseButton(
|
||||
Icon(
|
||||
symbol = MaterialSymbols.PlayArrow,
|
||||
modifier = Size50Modifier,
|
||||
contentDescription = stringRes(R.string.play),
|
||||
contentDescription = stringRes(Res.string.play),
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Pause,
|
||||
modifier = Size50Modifier,
|
||||
contentDescription = stringRes(R.string.pause),
|
||||
contentDescription = stringRes(Res.string.pause),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+10
-5
@@ -52,12 +52,17 @@ import androidx.media3.common.Tracks
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.captions_turn_off
|
||||
import com.vitorpamplona.amethyst.commons.resources.captions_turn_on
|
||||
import com.vitorpamplona.amethyst.commons.resources.cast_stop_casting
|
||||
import com.vitorpamplona.amethyst.commons.resources.cast_to_device
|
||||
import com.vitorpamplona.amethyst.commons.resources.download_to_phone
|
||||
import com.vitorpamplona.amethyst.commons.resources.picture_in_picture
|
||||
import com.vitorpamplona.amethyst.commons.resources.share_or_save
|
||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
||||
import com.vitorpamplona.amethyst.model.VideoButtonLocation
|
||||
import com.vitorpamplona.amethyst.model.VideoPlayerAction
|
||||
@@ -306,7 +311,7 @@ fun RenderTopButtons(
|
||||
val buttonItems by accountViewModel.videoPlayerButtonItemsFlow().collectAsStateWithLifecycle()
|
||||
val captionsIcon = if (captionsEnabled) MaterialSymbols.ClosedCaption else MaterialSymbols.ClosedCaptionDisabled
|
||||
val captionsContentDescription =
|
||||
stringRes(if (captionsEnabled) R.string.captions_turn_off else R.string.captions_turn_on)
|
||||
stringRes(if (captionsEnabled) Res.string.captions_turn_off else Res.string.captions_turn_on)
|
||||
val shareDialogVisible = remember { mutableStateOf(false) }
|
||||
val castDialogVisible = remember { mutableStateOf(false) }
|
||||
val castSessionState by Amethyst.instance.castRegistry.sessionState
|
||||
@@ -315,7 +320,7 @@ fun RenderTopButtons(
|
||||
(castSessionState as? CastSessionState.Casting)?.request?.url == mediaData.videoUri
|
||||
val castIcon = if (isThisVideoCasting) MaterialSymbols.CastConnected else MaterialSymbols.Cast
|
||||
val castContentDescription =
|
||||
stringRes(if (isThisVideoCasting) R.string.cast_stop_casting else R.string.cast_to_device)
|
||||
stringRes(if (isThisVideoCasting) Res.string.cast_stop_casting else Res.string.cast_to_device)
|
||||
val onCastButtonClick =
|
||||
remember(isThisVideoCasting) {
|
||||
{
|
||||
@@ -419,7 +424,7 @@ fun RenderTopButtons(
|
||||
AnimatedTopBarIconButton(
|
||||
controllerVisible = controllerVisible,
|
||||
symbol = MaterialSymbols.Share,
|
||||
contentDescription = stringRes(R.string.share_or_save),
|
||||
contentDescription = stringRes(Res.string.share_or_save),
|
||||
onClick = { shareDialogVisible.value = true },
|
||||
)
|
||||
}
|
||||
@@ -428,7 +433,7 @@ fun RenderTopButtons(
|
||||
AnimatedTopBarIconButton(
|
||||
controllerVisible = controllerVisible,
|
||||
symbol = MaterialSymbols.SaveAlt,
|
||||
contentDescription = stringRes(R.string.download_to_phone),
|
||||
contentDescription = stringRes(Res.string.download_to_phone),
|
||||
onClick = saveAction,
|
||||
)
|
||||
}
|
||||
|
||||
+5
-3
@@ -46,9 +46,11 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.download_to_phone
|
||||
import com.vitorpamplona.amethyst.commons.resources.video_download_has_started_toast
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize
|
||||
@@ -100,7 +102,7 @@ fun rememberSaveMediaAction(onSaveClick: (Context) -> Unit): () -> Unit {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
stringRes(context, R.string.video_download_has_started_toast),
|
||||
stringRes(Res.string.video_download_has_started_toast),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
@@ -131,7 +133,7 @@ fun SaveMediaButton(onSaveClick: (localContext: Context) -> Unit) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Download,
|
||||
modifier = Size20Modifier,
|
||||
contentDescription = stringRes(R.string.download_to_phone),
|
||||
contentDescription = stringRes(Res.string.download_to_phone),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -38,9 +38,10 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.share_or_save
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize
|
||||
@@ -99,7 +100,7 @@ fun ShareButton(innerAction: @Composable (MutableState<Boolean>, () -> Unit) ->
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Share,
|
||||
modifier = Size20Modifier,
|
||||
contentDescription = stringRes(R.string.share_or_save),
|
||||
contentDescription = stringRes(Res.string.share_or_save),
|
||||
)
|
||||
|
||||
innerAction(popupExpanded) { popupExpanded.value = false }
|
||||
|
||||
+4
-2
@@ -33,9 +33,11 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.skip_back
|
||||
import com.vitorpamplona.amethyst.commons.resources.skip_forward
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.SKIP_SECONDS
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
@@ -89,7 +91,7 @@ fun SkipButton(
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val icon = if (isForward) MaterialSymbols.Forward10 else MaterialSymbols.Replay10
|
||||
val label = if (isForward) stringRes(R.string.skip_forward, SKIP_SECONDS) else stringRes(R.string.skip_back, SKIP_SECONDS)
|
||||
val label = if (isForward) stringRes(Res.string.skip_forward, SKIP_SECONDS) else stringRes(Res.string.skip_back, SKIP_SECONDS)
|
||||
val tint = if (enabled) Color.White else Color.White.copy(alpha = 0.38f)
|
||||
IconButton(onClick = onClick, enabled = enabled, modifier = Modifier.size(48.dp)) {
|
||||
Icon(symbol = icon, contentDescription = label, tint = tint, modifier = Modifier.size(32.dp))
|
||||
|
||||
+10
-6
@@ -27,8 +27,12 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.drawable.Icon
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.mute_button
|
||||
import com.vitorpamplona.amethyst.commons.resources.muted_button
|
||||
import com.vitorpamplona.amethyst.commons.resources.pause
|
||||
import com.vitorpamplona.amethyst.commons.resources.play
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
|
||||
const val ACTION_MUTE = "com.vitorpamplona.amethyst.MUTE"
|
||||
const val ACTION_PLAY_PAUSE = "com.vitorpamplona.amethyst.PLAY_PAUSE"
|
||||
@@ -51,7 +55,7 @@ class ActionReceiver(
|
||||
}
|
||||
}
|
||||
|
||||
fun createMuteAction(
|
||||
suspend fun createMuteAction(
|
||||
context: Context,
|
||||
isMuted: Boolean,
|
||||
): RemoteAction {
|
||||
@@ -61,7 +65,7 @@ fun createMuteAction(
|
||||
} else {
|
||||
Icon.createWithResource(context, androidx.media3.session.R.drawable.media3_icon_volume_off)
|
||||
}
|
||||
val title = if (isMuted) stringRes(context, R.string.muted_button) else stringRes(context, R.string.mute_button)
|
||||
val title = if (isMuted) loadStringRes(Res.string.muted_button) else loadStringRes(Res.string.mute_button)
|
||||
|
||||
val intent =
|
||||
PendingIntent.getBroadcast(
|
||||
@@ -73,7 +77,7 @@ fun createMuteAction(
|
||||
return RemoteAction(icon, title, title, intent)
|
||||
}
|
||||
|
||||
fun createPlayPauseAction(
|
||||
suspend fun createPlayPauseAction(
|
||||
context: Context,
|
||||
isPlaying: Boolean,
|
||||
): RemoteAction {
|
||||
@@ -83,7 +87,7 @@ fun createPlayPauseAction(
|
||||
} else {
|
||||
Icon.createWithResource(context, androidx.media3.session.R.drawable.media3_icon_pause)
|
||||
}
|
||||
val title = if (!isPlaying) stringRes(context, R.string.play) else stringRes(context, R.string.pause)
|
||||
val title = if (!isPlaying) loadStringRes(Res.string.play) else loadStringRes(Res.string.pause)
|
||||
val intent =
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
|
||||
@@ -20,12 +20,22 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.pow
|
||||
|
||||
import android.content.Context
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_estimate_days
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_estimate_hours
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_estimate_instant
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_estimate_minutes
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_estimate_seconds
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_time_left
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_time_left_soon
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWEstimator
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWPolicy
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadPluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import org.jetbrains.compose.resources.PluralStringResource
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
/**
|
||||
@@ -46,21 +56,20 @@ suspend fun deviceHashesPerSecond(): Double = PoWEstimator.hashesPerSecond(PoWPo
|
||||
* and are the statistical mean of a memoryless search — any single post can be
|
||||
* much luckier or unluckier, so always present these as approximations.
|
||||
*/
|
||||
fun formatApproxDuration(
|
||||
context: Context,
|
||||
seconds: Double,
|
||||
): String {
|
||||
@Composable
|
||||
fun formatApproxDuration(seconds: Double): String {
|
||||
@Composable
|
||||
fun quantity(
|
||||
id: Int,
|
||||
id: PluralStringResource,
|
||||
count: Long,
|
||||
) = pluralStringRes(context, id, count.toInt(), count.toInt())
|
||||
) = pluralStringRes(id, count.toInt(), count.toInt())
|
||||
|
||||
return when {
|
||||
seconds < 1.0 -> stringRes(context, R.string.pow_estimate_instant)
|
||||
seconds < 90.0 -> quantity(R.plurals.pow_estimate_seconds, seconds.roundToLong())
|
||||
seconds < 90.0 * 60.0 -> quantity(R.plurals.pow_estimate_minutes, (seconds / 60.0).roundToLong())
|
||||
seconds < 48.0 * 3600.0 -> quantity(R.plurals.pow_estimate_hours, (seconds / 3600.0).roundToLong())
|
||||
else -> quantity(R.plurals.pow_estimate_days, (seconds / 86400.0).roundToLong())
|
||||
seconds < 1.0 -> stringRes(Res.string.pow_estimate_instant)
|
||||
seconds < 90.0 -> quantity(Res.plurals.pow_estimate_seconds, seconds.roundToLong())
|
||||
seconds < 90.0 * 60.0 -> quantity(Res.plurals.pow_estimate_minutes, (seconds / 60.0).roundToLong())
|
||||
seconds < 48.0 * 3600.0 -> quantity(Res.plurals.pow_estimate_hours, (seconds / 3600.0).roundToLong())
|
||||
else -> quantity(Res.plurals.pow_estimate_days, (seconds / 86400.0).roundToLong())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,15 +78,45 @@ fun formatApproxDuration(
|
||||
* "any moment now" once past it — a memoryless search has no shrinking
|
||||
* remainder, so past the mean the only honest claim is "soon".
|
||||
*/
|
||||
@Composable
|
||||
fun formatTimeLeft(
|
||||
context: Context,
|
||||
expectedSec: Double,
|
||||
elapsedSec: Long,
|
||||
): String {
|
||||
val remaining = expectedSec - elapsedSec
|
||||
return if (remaining > 1.0) {
|
||||
stringRes(context, R.string.pow_time_left, formatApproxDuration(context, remaining))
|
||||
stringRes(Res.string.pow_time_left, formatApproxDuration(remaining))
|
||||
} else {
|
||||
stringRes(context, R.string.pow_time_left_soon)
|
||||
stringRes(Res.string.pow_time_left_soon)
|
||||
}
|
||||
}
|
||||
|
||||
/** Suspend twin of [formatApproxDuration] for the mining foreground service,
|
||||
* which builds its notification text outside composition. */
|
||||
suspend fun loadApproxDuration(seconds: Double): String {
|
||||
suspend fun quantity(
|
||||
id: PluralStringResource,
|
||||
count: Long,
|
||||
) = loadPluralStringRes(id, count.toInt(), count.toInt())
|
||||
|
||||
return when {
|
||||
seconds < 1.0 -> loadStringRes(Res.string.pow_estimate_instant)
|
||||
seconds < 90.0 -> quantity(Res.plurals.pow_estimate_seconds, seconds.roundToLong())
|
||||
seconds < 90.0 * 60.0 -> quantity(Res.plurals.pow_estimate_minutes, (seconds / 60.0).roundToLong())
|
||||
seconds < 48.0 * 3600.0 -> quantity(Res.plurals.pow_estimate_hours, (seconds / 3600.0).roundToLong())
|
||||
else -> quantity(Res.plurals.pow_estimate_days, (seconds / 86400.0).roundToLong())
|
||||
}
|
||||
}
|
||||
|
||||
/** Suspend twin of [formatTimeLeft]; see [loadApproxDuration]. */
|
||||
suspend fun loadTimeLeft(
|
||||
expectedSec: Double,
|
||||
elapsedSec: Long,
|
||||
): String {
|
||||
val remaining = expectedSec - elapsedSec
|
||||
return if (remaining > 1.0) {
|
||||
loadStringRes(Res.string.pow_time_left, loadApproxDuration(remaining))
|
||||
} else {
|
||||
loadStringRes(Res.string.pow_time_left_soon)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,15 @@
|
||||
package com.vitorpamplona.amethyst.service.pow
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.boost
|
||||
import com.vitorpamplona.amethyst.commons.resources.post
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_kind_chat_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_kind_report
|
||||
import com.vitorpamplona.amethyst.commons.resources.private_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.reaction
|
||||
import com.vitorpamplona.amethyst.commons.resources.voice_post
|
||||
import com.vitorpamplona.amethyst.commons.resources.voice_reply
|
||||
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
|
||||
import com.vitorpamplona.quartz.nip18Reposts.RepostEvent
|
||||
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||
@@ -40,12 +48,12 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
||||
@StringRes
|
||||
fun powKindLabelRes(kind: Int): Int =
|
||||
when (kind) {
|
||||
ReactionEvent.KIND -> R.string.reaction
|
||||
RepostEvent.KIND, GenericRepostEvent.KIND -> R.string.boost
|
||||
VoiceEvent.KIND -> R.string.voice_post
|
||||
VoiceReplyEvent.KIND -> R.string.voice_reply
|
||||
ReportEvent.KIND -> R.string.pow_kind_report
|
||||
GiftWrapEvent.KIND -> R.string.private_message
|
||||
ChannelMessageEvent.KIND, LiveActivitiesChatMessageEvent.KIND -> R.string.pow_kind_chat_message
|
||||
else -> R.string.post
|
||||
ReactionEvent.KIND -> Res.string.reaction
|
||||
RepostEvent.KIND, GenericRepostEvent.KIND -> Res.string.boost
|
||||
VoiceEvent.KIND -> Res.string.voice_post
|
||||
VoiceReplyEvent.KIND -> Res.string.voice_reply
|
||||
ReportEvent.KIND -> Res.string.pow_kind_report
|
||||
GiftWrapEvent.KIND -> Res.string.private_message
|
||||
ChannelMessageEvent.KIND, LiveActivitiesChatMessageEvent.KIND -> Res.string.pow_kind_chat_message
|
||||
else -> Res.string.post
|
||||
}
|
||||
|
||||
+20
-13
@@ -23,12 +23,19 @@ package com.vitorpamplona.amethyst.service.pow
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_mining_job
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_mining_progress
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_mining_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_notification_cancel_all
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_notification_channel_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_notification_channel_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_notification_send_without_pow
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWEstimator
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWJobState
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadPluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.service.foreground.FlowProgressForegroundService
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -51,13 +58,13 @@ import kotlinx.coroutines.launch
|
||||
class PowMiningForegroundService : FlowProgressForegroundService<ImmutableList<PoWJobState>>() {
|
||||
override val fgsType: Int = ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
|
||||
override val channelId = CHANNEL_ID
|
||||
override val channelNameRes = R.string.pow_notification_channel_name
|
||||
override val channelDescRes = R.string.pow_notification_channel_description
|
||||
override val channelNameRes = Res.string.pow_notification_channel_name
|
||||
override val channelDescRes = Res.string.pow_notification_channel_description
|
||||
override val notificationId = NOTIFICATION_ID
|
||||
override val cancelAction = ACTION_CANCEL_ALL
|
||||
override val cancelLabelRes = R.string.pow_notification_cancel_all
|
||||
override val cancelLabelRes = Res.string.pow_notification_cancel_all
|
||||
override val secondaryAction = ACTION_SEND_ALL_NOW
|
||||
override val secondaryLabelRes = R.string.pow_notification_send_without_pow
|
||||
override val secondaryLabelRes = Res.string.pow_notification_send_without_pow
|
||||
|
||||
// clock-driven refresh for the time-left text and bar; the shortService budget (~3 min)
|
||||
// caps this at a handful of updates.
|
||||
@@ -101,7 +108,7 @@ class PowMiningForegroundService : FlowProgressForegroundService<ImmutableList<P
|
||||
lastQueueSize = value.size
|
||||
}
|
||||
|
||||
override fun render(value: ImmutableList<PoWJobState>): Content {
|
||||
override suspend fun render(value: ImmutableList<PoWJobState>): Content {
|
||||
val done = (sessionTotal - value.size).coerceAtLeast(0)
|
||||
val total = (done + value.size).coerceAtLeast(1)
|
||||
|
||||
@@ -116,11 +123,11 @@ class PowMiningForegroundService : FlowProgressForegroundService<ImmutableList<P
|
||||
|
||||
val base =
|
||||
current?.let {
|
||||
pluralStringRes(this, R.plurals.pow_mining_job, it.difficulty, stringRes(this, powKindLabelRes(it.kind)), it.difficulty)
|
||||
} ?: stringRes(this, R.string.pow_mining_title)
|
||||
loadPluralStringRes(Res.plurals.pow_mining_job, it.difficulty, loadStringRes(powKindLabelRes(it.kind)), it.difficulty)
|
||||
} ?: loadStringRes(Res.string.pow_mining_title)
|
||||
val text =
|
||||
if (expectedSec != null && elapsedSec != null) {
|
||||
"$base • ${formatTimeLeft(this, expectedSec, elapsedSec)}"
|
||||
"$base • ${loadTimeLeft(expectedSec, elapsedSec)}"
|
||||
} else {
|
||||
base
|
||||
}
|
||||
@@ -129,9 +136,9 @@ class PowMiningForegroundService : FlowProgressForegroundService<ImmutableList<P
|
||||
|
||||
val title =
|
||||
if (value.size > 1) {
|
||||
pluralStringRes(this, R.plurals.pow_mining_progress, value.size, value.size)
|
||||
loadPluralStringRes(Res.plurals.pow_mining_progress, value.size, value.size)
|
||||
} else {
|
||||
stringRes(this, R.string.pow_mining_title)
|
||||
loadStringRes(Res.string.pow_mining_title)
|
||||
}
|
||||
|
||||
val bar =
|
||||
|
||||
+19
-10
@@ -26,9 +26,18 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.relayauth.AuthPurposeKind
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_purpose_my_inbox
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_purpose_my_own_relay
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_purpose_notify_inbox
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_purpose_other
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_purpose_post_venue
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_purpose_read_outbox
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_purpose_read_venue
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_purpose_send_dm
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_purpose_thread
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
@@ -60,13 +69,13 @@ internal fun LoadRelayAuthUser(
|
||||
*/
|
||||
internal fun relayAuthPurposeLabelRes(kind: AuthPurposeKind): Int =
|
||||
when (kind) {
|
||||
AuthPurposeKind.SEND_DM -> R.string.relay_auth_purpose_send_dm
|
||||
AuthPurposeKind.NOTIFY_INBOX -> R.string.relay_auth_purpose_notify_inbox
|
||||
AuthPurposeKind.READ_OUTBOX -> R.string.relay_auth_purpose_read_outbox
|
||||
AuthPurposeKind.POST_VENUE -> R.string.relay_auth_purpose_post_venue
|
||||
AuthPurposeKind.READ_VENUE -> R.string.relay_auth_purpose_read_venue
|
||||
AuthPurposeKind.MY_INBOX -> R.string.relay_auth_purpose_my_inbox
|
||||
AuthPurposeKind.THREAD -> R.string.relay_auth_purpose_thread
|
||||
AuthPurposeKind.MY_OWN_RELAY -> R.string.relay_auth_purpose_my_own_relay
|
||||
AuthPurposeKind.OTHER -> R.string.relay_auth_purpose_other
|
||||
AuthPurposeKind.SEND_DM -> Res.string.relay_auth_purpose_send_dm
|
||||
AuthPurposeKind.NOTIFY_INBOX -> Res.string.relay_auth_purpose_notify_inbox
|
||||
AuthPurposeKind.READ_OUTBOX -> Res.string.relay_auth_purpose_read_outbox
|
||||
AuthPurposeKind.POST_VENUE -> Res.string.relay_auth_purpose_post_venue
|
||||
AuthPurposeKind.READ_VENUE -> Res.string.relay_auth_purpose_read_venue
|
||||
AuthPurposeKind.MY_INBOX -> Res.string.relay_auth_purpose_my_inbox
|
||||
AuthPurposeKind.THREAD -> Res.string.relay_auth_purpose_thread
|
||||
AuthPurposeKind.MY_OWN_RELAY -> Res.string.relay_auth_purpose_my_own_relay
|
||||
AuthPurposeKind.OTHER -> Res.string.relay_auth_purpose_other
|
||||
}
|
||||
|
||||
+21
-11
@@ -63,7 +63,6 @@ import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.Channel
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.auth.RelayAuthPrompt
|
||||
@@ -72,8 +71,19 @@ import com.vitorpamplona.amethyst.commons.relayauth.AuthPurpose
|
||||
import com.vitorpamplona.amethyst.commons.relayauth.AuthPurposeKind
|
||||
import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthPolicy
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.cancel
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_also_holding_back
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_always_allow_everywhere
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_always_everywhere_body
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_always_everywhere_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_always_log_in
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_log_in
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_login_as
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_name_and_n_others
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_never
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_never_allow_everywhere
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_never_everywhere_body
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_never_everywhere_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_not_now
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_relay_asks
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_auth_remember_relay
|
||||
@@ -300,11 +310,11 @@ private fun RelayAuthPromptDialog(
|
||||
} else {
|
||||
ButtonDefaults.outlinedButtonColors()
|
||||
},
|
||||
) { Text(if (rememberRelay) stringRes(R.string.relay_auth_never) else stringRes(Res.string.relay_auth_not_now)) }
|
||||
) { Text(if (rememberRelay) stringRes(Res.string.relay_auth_never) else stringRes(Res.string.relay_auth_not_now)) }
|
||||
Button(
|
||||
onClick = { onChoice(if (rememberRelay) UserAuthChoice.ALWAYS_ALLOW else UserAuthChoice.ALLOW_ONCE) },
|
||||
modifier = Modifier.weight(1f),
|
||||
) { Text(if (rememberRelay) stringRes(R.string.relay_auth_always_log_in) else stringRes(Res.string.relay_auth_log_in)) }
|
||||
) { Text(if (rememberRelay) stringRes(Res.string.relay_auth_always_log_in) else stringRes(Res.string.relay_auth_log_in)) }
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -313,10 +323,10 @@ private fun RelayAuthPromptDialog(
|
||||
TextButton(
|
||||
onClick = { confirming = UserAuthChoice.NEVER_ALLOW_EVERYWHERE },
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error),
|
||||
) { Text(stringRes(R.string.relay_auth_never_allow_everywhere), style = MaterialTheme.typography.labelMedium) }
|
||||
) { Text(stringRes(Res.string.relay_auth_never_allow_everywhere), style = MaterialTheme.typography.labelMedium) }
|
||||
TextButton(
|
||||
onClick = { confirming = UserAuthChoice.ALWAYS_ALLOW_EVERYWHERE },
|
||||
) { Text(stringRes(R.string.relay_auth_always_allow_everywhere), style = MaterialTheme.typography.labelMedium) }
|
||||
) { Text(stringRes(Res.string.relay_auth_always_allow_everywhere), style = MaterialTheme.typography.labelMedium) }
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -343,12 +353,12 @@ private fun PolicyEverywhereConfirmation(
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(stringRes(if (always) R.string.relay_auth_always_everywhere_title else R.string.relay_auth_never_everywhere_title))
|
||||
Text(stringRes(if (always) Res.string.relay_auth_always_everywhere_title else Res.string.relay_auth_never_everywhere_title))
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
stringRes(
|
||||
if (always) R.string.relay_auth_always_everywhere_body else R.string.relay_auth_never_everywhere_body,
|
||||
if (always) Res.string.relay_auth_always_everywhere_body else Res.string.relay_auth_never_everywhere_body,
|
||||
accountName,
|
||||
),
|
||||
)
|
||||
@@ -370,10 +380,10 @@ private fun PolicyEverywhereConfirmation(
|
||||
)
|
||||
},
|
||||
) {
|
||||
Text(stringRes(if (always) R.string.relay_auth_always_allow_everywhere else R.string.relay_auth_never_allow_everywhere))
|
||||
Text(stringRes(if (always) Res.string.relay_auth_always_allow_everywhere else Res.string.relay_auth_never_allow_everywhere))
|
||||
}
|
||||
},
|
||||
dismissButton = { TextButton(onClick = onDismiss) { Text(stringRes(R.string.cancel)) } },
|
||||
dismissButton = { TextButton(onClick = onDismiss) { Text(stringRes(Res.string.cancel)) } },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -558,7 +568,7 @@ private fun secondaryLine(
|
||||
): String? {
|
||||
val others = purposes.filter { it !== primary }
|
||||
if (others.isEmpty()) return null
|
||||
return pluralStringResource(R.plurals.relay_auth_also_holding_back, others.size, others.size)
|
||||
return pluralStringResource(Res.plurals.relay_auth_also_holding_back, others.size, others.size)
|
||||
}
|
||||
|
||||
/** A short label for a set of counterparties: the first person's name, or "Alice and 4 others". */
|
||||
@@ -571,7 +581,7 @@ private fun counterpartyLabel(
|
||||
val name = rememberCounterpartyName(first, accountViewModel)
|
||||
if (pubkeys.size == 1) return name
|
||||
val others = pubkeys.size - 1
|
||||
return pluralStringResource(R.plurals.relay_auth_name_and_n_others, others, name, others)
|
||||
return pluralStringResource(Res.plurals.relay_auth_name_and_n_others, others, name, others)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+3
-2
@@ -22,8 +22,9 @@ package com.vitorpamplona.amethyst.service.relayClient.notifyCommand.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.notify.NotifyRequestsCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.payment_required_title
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
@@ -50,7 +51,7 @@ fun DisplayNotifyMessages(
|
||||
NotifyRequestDialog(
|
||||
title =
|
||||
stringRes(
|
||||
id = R.string.payment_required_title,
|
||||
id = Res.string.payment_required_title,
|
||||
request.relayUrl.displayUrl(),
|
||||
),
|
||||
textContent = request.description,
|
||||
|
||||
+2
-2
@@ -36,12 +36,12 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_dialog_button_ok
|
||||
import com.vitorpamplona.amethyst.commons.resources.notify_block_relay
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
|
||||
@@ -119,7 +119,7 @@ fun NotifyRequestDialog(
|
||||
)
|
||||
Spacer(StdHorzSpacer)
|
||||
Text(
|
||||
text = stringRes(R.string.notify_block_relay),
|
||||
text = stringRes(Res.string.notify_block_relay),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
|
||||
+12
-7
@@ -32,14 +32,19 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.collectMemorySnapshot
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.resource_usage_alert_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.resource_usage_alert_not_now
|
||||
import com.vitorpamplona.amethyst.commons.resources.resource_usage_alert_opt_out
|
||||
import com.vitorpamplona.amethyst.commons.resources.resource_usage_alert_send
|
||||
import com.vitorpamplona.amethyst.commons.resources.resource_usage_alert_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.resource_usage_reason_bg_data
|
||||
import com.vitorpamplona.amethyst.commons.resources.resource_usage_reason_churn
|
||||
import com.vitorpamplona.amethyst.commons.resources.resource_usage_reason_conn_time
|
||||
import com.vitorpamplona.amethyst.commons.resources.resource_usage_reason_reconnects
|
||||
import com.vitorpamplona.amethyst.commons.resources.resource_usage_reason_wakelock
|
||||
import com.vitorpamplona.amethyst.commons.service.crashreports.DEV_REPORT_PUBKEY
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage
|
||||
@@ -111,7 +116,7 @@ fun DisplayResourceUsageAlert(
|
||||
text = {
|
||||
Text(
|
||||
stringRes(
|
||||
R.string.resource_usage_alert_message,
|
||||
Res.string.resource_usage_alert_message,
|
||||
reasonDescription(found),
|
||||
),
|
||||
)
|
||||
@@ -163,32 +168,32 @@ private fun reasonDescription(alert: ResourceUsageAlerts.Alert): String =
|
||||
when (alert.reason) {
|
||||
ResourceUsageAlerts.Reason.BACKGROUND_MOBILE_DATA ->
|
||||
stringRes(
|
||||
R.string.resource_usage_reason_bg_data,
|
||||
Res.string.resource_usage_reason_bg_data,
|
||||
ResourceUsageReportAssembler.formatBytes(alert.value),
|
||||
)
|
||||
|
||||
ResourceUsageAlerts.Reason.BACKGROUND_MOBILE_CONNECTION_TIME ->
|
||||
stringRes(
|
||||
R.string.resource_usage_reason_conn_time,
|
||||
Res.string.resource_usage_reason_conn_time,
|
||||
ResourceUsageReportAssembler.formatConnHours(alert.value),
|
||||
)
|
||||
|
||||
ResourceUsageAlerts.Reason.WAKELOCK_TIME ->
|
||||
stringRes(
|
||||
R.string.resource_usage_reason_wakelock,
|
||||
Res.string.resource_usage_reason_wakelock,
|
||||
ResourceUsageReportAssembler.formatDurationMs(alert.value),
|
||||
)
|
||||
|
||||
ResourceUsageAlerts.Reason.PROCESS_CHURN ->
|
||||
pluralStringResource(
|
||||
R.plurals.resource_usage_reason_churn,
|
||||
Res.plurals.resource_usage_reason_churn,
|
||||
alert.value.toInt(),
|
||||
alert.value.toInt(),
|
||||
)
|
||||
|
||||
ResourceUsageAlerts.Reason.RECONNECT_CHURN ->
|
||||
pluralStringResource(
|
||||
R.plurals.resource_usage_reason_reconnects,
|
||||
Res.plurals.resource_usage_reason_reconnects,
|
||||
alert.value.toInt(),
|
||||
alert.value.toInt(),
|
||||
)
|
||||
|
||||
+19
-12
@@ -28,11 +28,18 @@ import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_scheduled_posts_channel_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_scheduled_posts_channel_id
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_notification_scheduled_posts_channel_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.scheduled_posts_error_prefix
|
||||
import com.vitorpamplona.amethyst.commons.resources.scheduled_posts_notification_failed_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.scheduled_posts_notification_sent_title
|
||||
import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPost
|
||||
import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPostNotifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.scheduledposts.extractContentPreview
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
/**
|
||||
* Android [ScheduledPostNotifier]: posts a user-visible system notification when a
|
||||
@@ -43,17 +50,17 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
||||
class AndroidScheduledPostNotifier(
|
||||
private val context: Context,
|
||||
) : ScheduledPostNotifier {
|
||||
override fun notifySent(post: ScheduledPost) {
|
||||
override suspend fun notifySent(post: ScheduledPost) {
|
||||
ensureChannel(context)
|
||||
post(
|
||||
context = context,
|
||||
notId = idFor(post.id),
|
||||
title = stringRes(context, R.string.scheduled_posts_notification_sent_title),
|
||||
title = loadStringRes(Res.string.scheduled_posts_notification_sent_title),
|
||||
body = extractContentPreview(post, 120),
|
||||
)
|
||||
}
|
||||
|
||||
override fun notifyFailed(
|
||||
override suspend fun notifyFailed(
|
||||
post: ScheduledPost,
|
||||
error: String?,
|
||||
) {
|
||||
@@ -63,23 +70,23 @@ class AndroidScheduledPostNotifier(
|
||||
if (error.isNullOrBlank()) {
|
||||
snippet
|
||||
} else {
|
||||
"$snippet\n${stringRes(context, R.string.scheduled_posts_error_prefix, error)}"
|
||||
"$snippet\n${loadStringRes(Res.string.scheduled_posts_error_prefix, error)}"
|
||||
}
|
||||
post(
|
||||
context = context,
|
||||
notId = idFor(post.id),
|
||||
title = stringRes(context, R.string.scheduled_posts_notification_failed_title),
|
||||
title = loadStringRes(Res.string.scheduled_posts_notification_failed_title),
|
||||
body = body,
|
||||
)
|
||||
}
|
||||
|
||||
private fun post(
|
||||
private suspend fun post(
|
||||
context: Context,
|
||||
notId: Int,
|
||||
title: String,
|
||||
body: String,
|
||||
) {
|
||||
val channelId = stringRes(context, R.string.app_notification_scheduled_posts_channel_id)
|
||||
val channelId = loadStringRes(Res.string.app_notification_scheduled_posts_channel_id)
|
||||
val tapIntent =
|
||||
Intent(context, MainActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
@@ -126,15 +133,15 @@ class AndroidScheduledPostNotifier(
|
||||
private var channel: NotificationChannel? = null
|
||||
private const val SCHEDULED_POST_NOT_ID_BASE = 0x70000
|
||||
|
||||
fun ensureChannel(context: Context) {
|
||||
suspend fun ensureChannel(context: Context) {
|
||||
if (channel != null) return
|
||||
channel =
|
||||
NotificationChannel(
|
||||
stringRes(context, R.string.app_notification_scheduled_posts_channel_id),
|
||||
stringRes(context, R.string.app_notification_scheduled_posts_channel_name),
|
||||
loadStringRes(Res.string.app_notification_scheduled_posts_channel_id),
|
||||
loadStringRes(Res.string.app_notification_scheduled_posts_channel_name),
|
||||
NotificationManager.IMPORTANCE_DEFAULT,
|
||||
).apply {
|
||||
description = stringRes(context, R.string.app_notification_scheduled_posts_channel_description)
|
||||
description = loadStringRes(Res.string.app_notification_scheduled_posts_channel_description)
|
||||
}
|
||||
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
nm.createNotificationChannel(channel!!)
|
||||
|
||||
+25
-15
@@ -23,7 +23,17 @@ package com.vitorpamplona.amethyst.service.uploads
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.model.Account
|
||||
@@ -115,7 +125,7 @@ class UploadOrchestrator {
|
||||
|
||||
if (bytes != null) {
|
||||
if (bytes.size > 80000) {
|
||||
return error(R.string.media_too_big_for_nip95)
|
||||
return error(Res.string.media_too_big_for_nip95)
|
||||
}
|
||||
|
||||
updateState(0.8, UploadingState.Hashing)
|
||||
@@ -132,11 +142,11 @@ class UploadOrchestrator {
|
||||
return finish(OrchestratorResult.NIP95Result(it, bytes, originalContentType, originalHash))
|
||||
},
|
||||
onFailure = {
|
||||
return error(R.string.could_not_check_downloaded_file, it.message ?: it.javaClass.simpleName)
|
||||
return error(Res.string.could_not_check_downloaded_file, it.message ?: it.javaClass.simpleName)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
return error(R.string.could_not_open_the_compressed_file)
|
||||
return error(Res.string.could_not_open_the_compressed_file)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,10 +194,10 @@ class UploadOrchestrator {
|
||||
okHttpClient = Amethyst.instance.roleBasedHttpClientBuilder::okHttpClientForUploads,
|
||||
)
|
||||
} catch (_: SignerExceptions.ReadOnlyException) {
|
||||
error(R.string.login_with_a_private_key_to_be_able_to_upload)
|
||||
error(Res.string.login_with_a_private_key_to_be_able_to_upload)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
error(R.string.failed_to_upload_media, e.message ?: e.javaClass.simpleName)
|
||||
error(Res.string.failed_to_upload_media, e.message ?: e.javaClass.simpleName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,13 +270,13 @@ class UploadOrchestrator {
|
||||
|
||||
finalState
|
||||
} catch (_: SignerExceptions.ReadOnlyException) {
|
||||
error(R.string.login_with_a_private_key_to_be_able_to_upload)
|
||||
error(Res.string.login_with_a_private_key_to_be_able_to_upload)
|
||||
} catch (e: BlossomPaymentException) {
|
||||
// BUD-07: the server wants payment before it will store the blob.
|
||||
error(R.string.blossom_payment_required, e.payment.reason ?: serverBaseUrl)
|
||||
error(Res.string.blossom_payment_required, e.payment.reason ?: serverBaseUrl)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
error(R.string.failed_to_upload_media, e.message?.ifBlank { null } ?: e.javaClass.simpleName)
|
||||
error(Res.string.failed_to_upload_media, e.message?.ifBlank { null } ?: e.javaClass.simpleName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,7 +328,7 @@ class UploadOrchestrator {
|
||||
okHttpClient: (String) -> OkHttpClient,
|
||||
): UploadingFinalState {
|
||||
if (uploadResult.url.isNullOrBlank()) {
|
||||
return error(R.string.server_did_not_provide_a_url_after_uploading)
|
||||
return error(Res.string.server_did_not_provide_a_url_after_uploading)
|
||||
}
|
||||
|
||||
updateState(0.6, UploadingState.Downloading)
|
||||
@@ -326,7 +336,7 @@ class UploadOrchestrator {
|
||||
// Use streaming verification for memory efficiency with large files
|
||||
val verification =
|
||||
ImageDownloader().waitAndVerifyStream(uploadResult.url, okHttpClient)
|
||||
?: return error(R.string.could_not_download_from_the_server)
|
||||
?: return error(Res.string.could_not_download_from_the_server)
|
||||
|
||||
updateState(0.8, UploadingState.Hashing)
|
||||
|
||||
@@ -458,10 +468,10 @@ class UploadOrchestrator {
|
||||
try {
|
||||
stripAfterCompression(uri, compressed, mimeType, compressionQuality, stripMetadata, onStrippingFailed, context)
|
||||
} catch (e: AvifMetadataNotVerifiableException) {
|
||||
return error(R.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName).also {
|
||||
return error(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName).also {
|
||||
deleteTempUri(compressed.uri, uri)
|
||||
}
|
||||
} ?: return error(R.string.upload_cancelled).also {
|
||||
} ?: return error(Res.string.upload_cancelled).also {
|
||||
deleteTempUri(compressed.uri, uri)
|
||||
}
|
||||
|
||||
@@ -500,10 +510,10 @@ class UploadOrchestrator {
|
||||
try {
|
||||
stripAfterCompression(uri, compressed, mimeType, compressionQuality, stripMetadata, onStrippingFailed, context)
|
||||
} catch (e: AvifMetadataNotVerifiableException) {
|
||||
return error(R.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName).also {
|
||||
return error(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName).also {
|
||||
deleteTempUri(compressed.uri, uri)
|
||||
}
|
||||
} ?: return error(R.string.upload_cancelled).also {
|
||||
} ?: return error(Res.string.upload_cancelled).also {
|
||||
deleteTempUri(compressed.uri, uri)
|
||||
}
|
||||
|
||||
|
||||
+12
-8
@@ -23,9 +23,13 @@ package com.vitorpamplona.amethyst.service.uploads.blossom
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_sync_cancel
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_sync_channel_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_sync_channel_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_syncing
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.service.foreground.FlowProgressForegroundService
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
/**
|
||||
* Foreground service that keeps the BUD-04 "sync all" sweep ([BlossomMirrorQueue]) running
|
||||
@@ -40,11 +44,11 @@ import com.vitorpamplona.amethyst.ui.stringRes
|
||||
class BlossomSyncForegroundService : FlowProgressForegroundService<BlossomSyncState?>() {
|
||||
override val fgsType: Int = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
override val channelId = CHANNEL_ID
|
||||
override val channelNameRes = R.string.blossom_sync_channel_name
|
||||
override val channelDescRes = R.string.blossom_sync_channel_description
|
||||
override val channelNameRes = Res.string.blossom_sync_channel_name
|
||||
override val channelDescRes = Res.string.blossom_sync_channel_description
|
||||
override val notificationId = NOTIFICATION_ID
|
||||
override val cancelAction = ACTION_CANCEL
|
||||
override val cancelLabelRes = R.string.blossom_sync_cancel
|
||||
override val cancelLabelRes = Res.string.blossom_sync_cancel
|
||||
|
||||
override fun state() = Amethyst.instance.blossomMirrorQueue.state
|
||||
|
||||
@@ -55,14 +59,14 @@ class BlossomSyncForegroundService : FlowProgressForegroundService<BlossomSyncSt
|
||||
// State emits on every mirror step (including currentHost changes), so no clock refresh.
|
||||
override val refreshMs: Long? = null
|
||||
|
||||
override fun render(value: BlossomSyncState?): Content {
|
||||
if (value == null) return Content(stringRes(this, R.string.blossom_syncing), null, Bar.Indeterminate)
|
||||
override suspend fun render(value: BlossomSyncState?): Content {
|
||||
if (value == null) return Content(loadStringRes(Res.string.blossom_syncing), null, Bar.Indeterminate)
|
||||
val text =
|
||||
buildString {
|
||||
append("${value.done} / ${value.total}")
|
||||
if (value.failed > 0) append(" · ${value.failed} failed")
|
||||
}
|
||||
return Content(stringRes(this, R.string.blossom_syncing), text, Bar.Determinate(value.fraction.toDouble()))
|
||||
return Content(loadStringRes(Res.string.blossom_syncing), text, Bar.Determinate(value.fraction.toDouble()))
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
+9
-7
@@ -25,14 +25,16 @@ import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import android.webkit.MimeTypeMap
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.upload.BlossomPaymentException
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.service.HttpStatusMessages
|
||||
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.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
@@ -215,11 +217,11 @@ class BlossomUploader {
|
||||
|
||||
val explanation = HttpStatusMessages.resourceIdFor(response.code)
|
||||
if (errorMessage != null) {
|
||||
throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, serverBaseUrl.displayUrl(), errorMessage))
|
||||
throw RuntimeException(loadStringRes(Res.string.failed_to_upload_to_server_with_message, serverBaseUrl.displayUrl(), errorMessage))
|
||||
} else if (explanation != null) {
|
||||
throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, serverBaseUrl.displayUrl(), stringRes(context, explanation)))
|
||||
throw RuntimeException(loadStringRes(Res.string.failed_to_upload_to_server_with_message, serverBaseUrl.displayUrl(), loadStringRes(explanation)))
|
||||
} else {
|
||||
throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, serverBaseUrl.displayUrl(), response.code.toString()))
|
||||
throw RuntimeException(loadStringRes(Res.string.failed_to_upload_to_server_with_message, serverBaseUrl.displayUrl(), response.code.toString()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,9 +276,9 @@ class BlossomUploader {
|
||||
} else {
|
||||
val explanation = HttpStatusMessages.resourceIdFor(response.code)
|
||||
if (explanation != null) {
|
||||
throw RuntimeException(stringRes(context, R.string.failed_to_delete_with_message, stringRes(context, explanation)))
|
||||
throw RuntimeException(loadStringRes(Res.string.failed_to_delete_with_message, loadStringRes(explanation)))
|
||||
} else {
|
||||
throw RuntimeException(stringRes(context, R.string.failed_to_delete_with_message, response.code))
|
||||
throw RuntimeException(loadStringRes(Res.string.failed_to_delete_with_message, response.code))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -23,7 +23,9 @@ package com.vitorpamplona.amethyst.service.uploads.blossom.bud10
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.no_blossom_apps_found_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.no_blossom_apps_found_title
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
fun openBlossomUriAsIntent(
|
||||
@@ -38,6 +40,6 @@ fun openBlossomUriAsIntent(
|
||||
context.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
onError(R.string.no_blossom_apps_found_title, R.string.no_blossom_apps_found_description)
|
||||
onError(Res.string.no_blossom_apps_found_title, Res.string.no_blossom_apps_found_description)
|
||||
}
|
||||
}
|
||||
|
||||
+11
-9
@@ -27,14 +27,16 @@ import android.provider.OpenableColumns
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.net.toFile
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.service.HttpStatusMessages
|
||||
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.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
|
||||
@@ -198,10 +200,10 @@ class Nip96Uploader {
|
||||
if (event != null) {
|
||||
convertToMediaResult(event)
|
||||
} else {
|
||||
throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, server.apiUrl.displayUrl(), result.message))
|
||||
throw RuntimeException(loadStringRes(Res.string.failed_to_upload_to_server_with_message, server.apiUrl.displayUrl(), result.message))
|
||||
}
|
||||
} else {
|
||||
throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, server.apiUrl.displayUrl(), result.message))
|
||||
throw RuntimeException(loadStringRes(Res.string.failed_to_upload_to_server_with_message, server.apiUrl.displayUrl(), result.message))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -223,11 +225,11 @@ class Nip96Uploader {
|
||||
|
||||
val explanation = HttpStatusMessages.resourceIdFor(response.code)
|
||||
if (errorMessage != null) {
|
||||
throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, server.apiUrl.displayUrl(), errorMessage))
|
||||
throw RuntimeException(loadStringRes(Res.string.failed_to_upload_to_server_with_message, server.apiUrl.displayUrl(), errorMessage))
|
||||
} else if (explanation != null) {
|
||||
throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, server.apiUrl.displayUrl(), stringRes(context, explanation)))
|
||||
throw RuntimeException(loadStringRes(Res.string.failed_to_upload_to_server_with_message, server.apiUrl.displayUrl(), loadStringRes(explanation)))
|
||||
} else {
|
||||
throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, server.apiUrl.displayUrl(), response.code.toString()))
|
||||
throw RuntimeException(loadStringRes(Res.string.failed_to_upload_to_server_with_message, server.apiUrl.displayUrl(), response.code.toString()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,9 +317,9 @@ class Nip96Uploader {
|
||||
} else {
|
||||
val explanation = HttpStatusMessages.resourceIdFor(response.code)
|
||||
if (explanation != null) {
|
||||
throw RuntimeException(stringRes(context, R.string.failed_to_delete_with_message, stringRes(context, explanation)))
|
||||
throw RuntimeException(loadStringRes(Res.string.failed_to_delete_with_message, loadStringRes(explanation)))
|
||||
} else {
|
||||
throw RuntimeException(stringRes(context, R.string.failed_to_delete_with_message, response.code))
|
||||
throw RuntimeException(loadStringRes(Res.string.failed_to_delete_with_message, response.code))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,6 @@ class MainActivity : AppCompatActivity() {
|
||||
Log.d("ActivityLifecycle") { "MainActivity.onCreate $this" }
|
||||
|
||||
setContent {
|
||||
StringResSetup()
|
||||
AmethystTheme {
|
||||
NowProvider {
|
||||
AccountScreen(Amethyst.instance.sessionManager)
|
||||
|
||||
@@ -20,69 +20,28 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.util.LruCache
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.PluralsRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import org.jetbrains.compose.resources.PluralStringResource
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes as commonsPluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes as commonsStringRes
|
||||
|
||||
/**
|
||||
* Cache for stringResource because it seems to be > 1ms function in some phones
|
||||
*/
|
||||
private val resourceCache = LruCache<Int, String>(300)
|
||||
private var resourceCacheLanguage: String? = null
|
||||
// Every user-facing string now lives in the commons Compose catalog
|
||||
// (com.vitorpamplona.amethyst.commons.resources.Res.string.*), so these are thin
|
||||
// aliases over the commons bridge. They exist only so the ~500 files that import
|
||||
// `com.vitorpamplona.amethyst.ui.stringRes` did not all have to change their import
|
||||
// in the same commit; a file moving to commons swaps this import for
|
||||
// `com.vitorpamplona.amethyst.commons.ui.stringRes` and nothing else.
|
||||
//
|
||||
// The Android `R.string` overloads that used to live here (and the LruCache behind
|
||||
// them, which existed because Resources.getString measured >1ms on some phones) are
|
||||
// gone with the last R.string reference. compose-resources parses each locale file
|
||||
// once into a process-wide cache, so repeat lookups are map hits.
|
||||
|
||||
// Caches most common icons in the app to avoid using disk
|
||||
private val iconCache = LruCache<Int, LruCache<Int, Painter>>(30)
|
||||
|
||||
fun resourceCacheInit() {
|
||||
resourceCache
|
||||
resourceCacheLanguage
|
||||
iconCache
|
||||
}
|
||||
|
||||
fun checkLanguage(currentLanguage: String) {
|
||||
if (resourceCacheLanguage == null) {
|
||||
resourceCacheLanguage = currentLanguage
|
||||
} else {
|
||||
if (resourceCacheLanguage != currentLanguage) {
|
||||
resourceCacheLanguage = currentLanguage
|
||||
resourceCache.evictAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StringResSetup() {
|
||||
val config = LocalConfiguration.current
|
||||
if (!config.locales.isEmpty) {
|
||||
val language = config.locales.get(0).language
|
||||
LifecycleResumeEffect(language) {
|
||||
checkLanguage(language)
|
||||
|
||||
onPauseOrDispose { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun stringRes(id: Int): String = resourceCache.get(id) ?: stringResource(id).also { resourceCache.put(id, it) }
|
||||
|
||||
// Overloads for keys already migrated to commons Compose resources
|
||||
// (com.vitorpamplona.amethyst.commons.resources.Res.string.*). They let a file
|
||||
// mix migrated and unmigrated keys under this one import — overload resolution
|
||||
// picks by argument type — so migrating a key is just R.string.x -> Res.string.x.
|
||||
// When a whole file moves to commons, swap this import for
|
||||
// com.vitorpamplona.amethyst.commons.ui.stringRes.
|
||||
@Composable
|
||||
fun stringRes(id: StringResource): String = commonsStringRes(id)
|
||||
|
||||
@@ -99,74 +58,14 @@ fun pluralStringRes(
|
||||
vararg args: Any,
|
||||
): String = if (args.isEmpty()) commonsPluralStringRes(id, count) else commonsPluralStringRes(id, count, *args)
|
||||
|
||||
@Composable
|
||||
fun stringRes(
|
||||
id: Int,
|
||||
vararg args: String,
|
||||
): String =
|
||||
String
|
||||
.format(
|
||||
LocalConfiguration.current.locales.get(0),
|
||||
resourceCache.get(id) ?: stringResource(id).also { resourceCache.put(id, it) },
|
||||
*args,
|
||||
)
|
||||
// Caches most common icons in the app to avoid using disk. Drawables are still
|
||||
// Android resources - only strings moved to the Compose catalog.
|
||||
private val iconCache = LruCache<Int, LruCache<Int, Painter>>(30)
|
||||
|
||||
@Composable
|
||||
fun stringRes(
|
||||
id: Int,
|
||||
vararg args: Int?,
|
||||
): String =
|
||||
String
|
||||
.format(
|
||||
LocalConfiguration.current.locales.get(0),
|
||||
resourceCache.get(id) ?: stringResource(id).also { resourceCache.put(id, it) },
|
||||
*args,
|
||||
)
|
||||
|
||||
fun stringRes(
|
||||
ctx: Context,
|
||||
id: Int,
|
||||
): String = resourceCache.get(id) ?: ctx.getString(id).also { resourceCache.put(id, it) }
|
||||
|
||||
fun stringRes(
|
||||
ctx: Context,
|
||||
id: Int,
|
||||
vararg args: String?,
|
||||
): String {
|
||||
val res = ctx.resources
|
||||
|
||||
return String
|
||||
.format(
|
||||
res.configuration.locales.get(0),
|
||||
resourceCache.get(id) ?: res.getString(id).also { resourceCache.put(id, it) },
|
||||
*args,
|
||||
)
|
||||
fun resourceCacheInit() {
|
||||
iconCache
|
||||
}
|
||||
|
||||
fun stringRes(
|
||||
ctx: Context,
|
||||
id: Int,
|
||||
vararg args: Int?,
|
||||
): String {
|
||||
val res = ctx.resources
|
||||
|
||||
return String
|
||||
.format(
|
||||
res.configuration.locales.get(0),
|
||||
resourceCache.get(id) ?: res.getString(id).also { resourceCache.put(id, it) },
|
||||
*args,
|
||||
)
|
||||
}
|
||||
|
||||
// Plural resolver for non-composable scope (e.g. onClick callbacks). Not cached:
|
||||
// the resolved string varies by `count` quantity and the resourceCache is keyed by id.
|
||||
fun pluralStringRes(
|
||||
ctx: Context,
|
||||
@PluralsRes id: Int,
|
||||
count: Int,
|
||||
vararg formatArgs: Any?,
|
||||
): String = ctx.resources.getQuantityString(id, count, *formatArgs)
|
||||
|
||||
/**
|
||||
* This cache can only be used if the painter is the only copy on the screen
|
||||
* It should store a separate Painter for each size. It's safe to just assume
|
||||
|
||||
@@ -68,7 +68,6 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil3.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
@@ -77,6 +76,7 @@ import com.vitorpamplona.amethyst.commons.resources.add_bitcoin_invoice
|
||||
import com.vitorpamplona.amethyst.commons.resources.cancel_bitcoin_invoice
|
||||
import com.vitorpamplona.amethyst.commons.resources.lightning_create_and_add_invoice
|
||||
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.service.playback.composable.VideoView
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
@@ -409,7 +409,7 @@ private fun MessageField(postViewModel: EditPostViewModel) {
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(R.string.what_s_on_your_mind),
|
||||
text = stringRes(Res.string.what_s_on_your_mind),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
|
||||
@@ -29,10 +29,15 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
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.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.currentWord
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.onUiThread
|
||||
@@ -48,7 +53,6 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent
|
||||
import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
@@ -66,6 +70,7 @@ import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.size
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.thumbhash
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Stable
|
||||
open class EditPostViewModel : ViewModel() {
|
||||
@@ -167,13 +172,17 @@ open class EditPostViewModel : ViewModel() {
|
||||
onError: (String, String) -> Unit,
|
||||
context: Context,
|
||||
stripMetadata: Boolean = true,
|
||||
) = try {
|
||||
uploadUnsafe(alt, sensitiveContent, mediaQuality, isPrivate, server, onError, context, stripMetadata)
|
||||
} catch (e: SignerExceptions.ReadOnlyException) {
|
||||
onError(
|
||||
stringRes(context, R.string.read_only_user),
|
||||
stringRes(context, R.string.login_with_a_private_key_to_be_able_to_sign_events),
|
||||
)
|
||||
) {
|
||||
try {
|
||||
uploadUnsafe(alt, sensitiveContent, mediaQuality, isPrivate, server, onError, context, stripMetadata)
|
||||
} catch (e: SignerExceptions.ReadOnlyException) {
|
||||
viewModelScope.launch {
|
||||
onError(
|
||||
loadStringRes(Res.string.read_only_user),
|
||||
loadStringRes(Res.string.login_with_a_private_key_to_be_able_to_sign_events),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun uploadUnsafe(
|
||||
@@ -256,9 +265,9 @@ open class EditPostViewModel : ViewModel() {
|
||||
|
||||
this@EditPostViewModel.multiOrchestrator = null
|
||||
} else {
|
||||
val errorMessages = results.errors.map { stringRes(context, it.errorResource, *it.params) }.distinct()
|
||||
val errorMessages = results.errors.map { loadStringRes(it.errorResource, *it.params) }.distinct()
|
||||
|
||||
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
||||
onError(loadStringRes(Res.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
||||
}
|
||||
} finally {
|
||||
mediaUploadTracker.finishUpload()
|
||||
|
||||
@@ -28,9 +28,13 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.model.VideoPostKind
|
||||
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.richtext.RichTextParser
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
|
||||
import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator
|
||||
@@ -41,7 +45,6 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -101,13 +104,17 @@ open class NewMediaModel : ViewModel() {
|
||||
accountViewModel: AccountViewModel,
|
||||
onSucess: () -> Unit,
|
||||
onError: (String, String) -> Unit,
|
||||
) = try {
|
||||
uploadUnsafe(context, accountViewModel, onSucess, onError)
|
||||
} catch (e: SignerExceptions.ReadOnlyException) {
|
||||
onError(
|
||||
stringRes(context, R.string.read_only_user),
|
||||
stringRes(context, R.string.login_with_a_private_key_to_be_able_to_sign_events),
|
||||
)
|
||||
) {
|
||||
try {
|
||||
uploadUnsafe(context, accountViewModel, onSucess, onError)
|
||||
} catch (e: SignerExceptions.ReadOnlyException) {
|
||||
viewModelScope.launch {
|
||||
onError(
|
||||
loadStringRes(Res.string.read_only_user),
|
||||
loadStringRes(Res.string.login_with_a_private_key_to_be_able_to_sign_events),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun uploadUnsafe(
|
||||
@@ -215,9 +222,9 @@ open class NewMediaModel : ViewModel() {
|
||||
onceUploaded()
|
||||
cancelModel()
|
||||
} else {
|
||||
val errorMessages = results.errors.map { stringRes(context, it.errorResource, *it.params) }.distinct()
|
||||
val errorMessages = results.errors.map { loadStringRes(it.errorResource, *it.params) }.distinct()
|
||||
|
||||
onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
||||
onError(loadStringRes(Res.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,17 +52,24 @@ 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.R
|
||||
import com.vitorpamplona.amethyst.commons.model.VideoPostKind
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_caption
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_caption_example
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_sensitive_content_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_sensitive_content_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.file_server
|
||||
import com.vitorpamplona.amethyst.commons.resources.file_server_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.media_compression_quality_explainer
|
||||
import com.vitorpamplona.amethyst.commons.resources.media_compression_quality_high
|
||||
import com.vitorpamplona.amethyst.commons.resources.media_compression_quality_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.media_compression_quality_low
|
||||
import com.vitorpamplona.amethyst.commons.resources.media_compression_quality_medium
|
||||
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.resources.video_codec_h265_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.video_codec_h265_label
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery
|
||||
@@ -203,8 +210,8 @@ fun ImageVideoPost(
|
||||
)
|
||||
|
||||
SettingSwitchItem(
|
||||
title = R.string.add_sensitive_content_label,
|
||||
description = R.string.add_sensitive_content_description,
|
||||
title = Res.string.add_sensitive_content_label,
|
||||
description = Res.string.add_sensitive_content_description,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -213,7 +220,7 @@ fun ImageVideoPost(
|
||||
onCheckedChange = { postViewModel.sensitiveContent = it },
|
||||
)
|
||||
|
||||
SettingsRow(R.string.file_server, R.string.file_server_description) {
|
||||
SettingsRow(Res.string.file_server, Res.string.file_server_description) {
|
||||
TextSpinner(
|
||||
label = "",
|
||||
placeholder =
|
||||
@@ -274,8 +281,8 @@ fun ImageVideoPost(
|
||||
// Only show H.265 codec option if there are videos in the upload
|
||||
if (postViewModel.multiOrchestrator?.hasVideo() == true) {
|
||||
SettingSwitchItem(
|
||||
title = R.string.video_codec_h265_label,
|
||||
description = R.string.video_codec_h265_description,
|
||||
title = Res.string.video_codec_h265_label,
|
||||
description = Res.string.video_codec_h265_description,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -286,8 +293,8 @@ fun ImageVideoPost(
|
||||
}
|
||||
|
||||
SettingSwitchItem(
|
||||
title = R.string.strip_metadata_label,
|
||||
description = R.string.strip_metadata_description,
|
||||
title = Res.string.strip_metadata_label,
|
||||
description = Res.string.strip_metadata_description,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
|
||||
+8
-5
@@ -51,7 +51,6 @@ 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.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
@@ -60,15 +59,19 @@ import com.vitorpamplona.amethyst.commons.resources.avatar_url
|
||||
import com.vitorpamplona.amethyst.commons.resources.banner_url
|
||||
import com.vitorpamplona.amethyst.commons.resources.clink_offer_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.display_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.github
|
||||
import com.vitorpamplona.amethyst.commons.resources.github_proof_url_template
|
||||
import com.vitorpamplona.amethyst.commons.resources.lightning_address
|
||||
import com.vitorpamplona.amethyst.commons.resources.lnurl
|
||||
import com.vitorpamplona.amethyst.commons.resources.mastodon
|
||||
import com.vitorpamplona.amethyst.commons.resources.mastodon_proof_url_template
|
||||
import com.vitorpamplona.amethyst.commons.resources.my_display_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.my_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.nip_05
|
||||
import com.vitorpamplona.amethyst.commons.resources.profile
|
||||
import com.vitorpamplona.amethyst.commons.resources.pronouns
|
||||
import com.vitorpamplona.amethyst.commons.resources.social_proof
|
||||
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
|
||||
@@ -104,7 +107,7 @@ fun NewUserMetadataScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SavingTopBar(
|
||||
titleRes = R.string.profile,
|
||||
titleRes = Res.string.profile,
|
||||
onCancel = {
|
||||
nav.popBack()
|
||||
},
|
||||
@@ -343,7 +346,7 @@ fun NewUserMetadataScreen(
|
||||
expanded = socialExpanded,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(R.string.twitter)) },
|
||||
label = { Text(text = stringRes(Res.string.twitter)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = postViewModel.twitter.value,
|
||||
onValueChange = { postViewModel.twitter.value = it },
|
||||
@@ -358,7 +361,7 @@ fun NewUserMetadataScreen(
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(R.string.mastodon)) },
|
||||
label = { Text(text = stringRes(Res.string.mastodon)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = postViewModel.mastodon.value,
|
||||
onValueChange = { postViewModel.mastodon.value = it },
|
||||
@@ -373,7 +376,7 @@ fun NewUserMetadataScreen(
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(R.string.github)) },
|
||||
label = { Text(text = stringRes(Res.string.github)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = postViewModel.github.value,
|
||||
onValueChange = { postViewModel.github.value = it },
|
||||
|
||||
+13
-8
@@ -26,7 +26,13 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.model.Account
|
||||
import com.vitorpamplona.amethyst.service.uploads.AvifMetadataNotVerifiableException
|
||||
import com.vitorpamplona.amethyst.service.uploads.CompressorQuality
|
||||
@@ -37,7 +43,6 @@ import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity
|
||||
@@ -223,8 +228,8 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
} catch (e: AvifMetadataNotVerifiableException) {
|
||||
isUploadingImageForPicture = false
|
||||
onError(
|
||||
stringRes(context, R.string.metadata_strip_failed_title),
|
||||
stringRes(context, R.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName),
|
||||
loadStringRes(Res.string.metadata_strip_failed_title),
|
||||
loadStringRes(Res.string.avif_metadata_strip_failed, e.message ?: e.javaClass.simpleName),
|
||||
)
|
||||
return null
|
||||
}
|
||||
@@ -238,8 +243,8 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
!strippingResult.stripped
|
||||
) {
|
||||
onError(
|
||||
stringRes(context, R.string.metadata_strip_failed_title),
|
||||
stringRes(context, R.string.metadata_strip_failed_upload_cancelled),
|
||||
loadStringRes(Res.string.metadata_strip_failed_title),
|
||||
loadStringRes(Res.string.metadata_strip_failed_upload_cancelled),
|
||||
)
|
||||
return null
|
||||
} else {
|
||||
@@ -278,13 +283,13 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
if (result.url == null) {
|
||||
onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.server_did_not_provide_a_url_after_uploading))
|
||||
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(stringRes(context, R.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName)
|
||||
onError(loadStringRes(Res.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName)
|
||||
null
|
||||
} finally {
|
||||
isUploadingImageForPicture = false
|
||||
|
||||
+4
-3
@@ -24,9 +24,10 @@ import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.cancel
|
||||
import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_body
|
||||
import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload
|
||||
import com.vitorpamplona.amethyst.service.uploads.ConfirmationCallbacks
|
||||
import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation
|
||||
@@ -42,7 +43,7 @@ fun StrippingFailureDialog(confirmation: SuspendableConfirmation) {
|
||||
fun StrippingFailureDialog(dialogState: ConfirmationCallbacks) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { dialogState.onCancel() },
|
||||
title = { Text(stringRes(R.string.metadata_strip_failed_title)) },
|
||||
title = { Text(stringRes(Res.string.metadata_strip_failed_title)) },
|
||||
text = { Text(stringRes(Res.string.metadata_strip_failed_body)) },
|
||||
confirmButton = {
|
||||
Button(onClick = { dialogState.onConfirm() }) {
|
||||
@@ -51,7 +52,7 @@ fun StrippingFailureDialog(dialogState: ConfirmationCallbacks) {
|
||||
},
|
||||
dismissButton = {
|
||||
Button(onClick = { dialogState.onCancel() }) {
|
||||
Text(stringRes(R.string.cancel))
|
||||
Text(stringRes(Res.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
+7
-5
@@ -52,12 +52,14 @@ 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.R
|
||||
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.add
|
||||
import com.vitorpamplona.amethyst.commons.resources.bolt12_offer
|
||||
import com.vitorpamplona.amethyst.commons.resources.bolt12_offers
|
||||
import com.vitorpamplona.amethyst.commons.resources.bolt12_offers_explainer
|
||||
import com.vitorpamplona.amethyst.commons.resources.bolt12_offers_section_explainer
|
||||
import com.vitorpamplona.amethyst.commons.resources.delete_bolt12_offer
|
||||
import com.vitorpamplona.amethyst.commons.resources.invalid_bolt12_offer
|
||||
import com.vitorpamplona.amethyst.commons.resources.no_bolt12_offers_message
|
||||
@@ -102,7 +104,7 @@ fun Bolt12OffersScaffold(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SavingTopBar(
|
||||
titleRes = R.string.bolt12_offers,
|
||||
titleRes = Res.string.bolt12_offers,
|
||||
onCancel = {
|
||||
viewModel.refresh()
|
||||
onClose()
|
||||
@@ -152,8 +154,8 @@ fun Bolt12OffersBody(viewModel: Bolt12OffersViewModel) {
|
||||
) {
|
||||
item {
|
||||
SettingsCategory(
|
||||
R.string.bolt12_offers,
|
||||
R.string.bolt12_offers_section_explainer,
|
||||
Res.string.bolt12_offers,
|
||||
Res.string.bolt12_offers_section_explainer,
|
||||
SettingsCategoryFirstModifier,
|
||||
)
|
||||
}
|
||||
@@ -251,7 +253,7 @@ fun Bolt12OfferAddField(onAdd: (raw: String) -> Boolean) {
|
||||
shape = ButtonBorder,
|
||||
enabled = offer.isNotBlank(),
|
||||
) {
|
||||
Text(text = stringRes(id = R.string.add), color = Color.White)
|
||||
Text(text = stringRes(id = Res.string.add), color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-6
@@ -59,10 +59,10 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.add_a_blossom_server
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_media_server
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_mirror_uploads
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_mirror_uploads_caption
|
||||
@@ -72,6 +72,9 @@ import com.vitorpamplona.amethyst.commons.resources.delete_media_server
|
||||
import com.vitorpamplona.amethyst.commons.resources.media_server_added
|
||||
import com.vitorpamplona.amethyst.commons.resources.media_server_primary_badge
|
||||
import com.vitorpamplona.amethyst.commons.resources.media_server_reorder
|
||||
import com.vitorpamplona.amethyst.commons.resources.media_server_status_offline
|
||||
import com.vitorpamplona.amethyst.commons.resources.media_server_status_online
|
||||
import com.vitorpamplona.amethyst.commons.resources.media_server_status_slow
|
||||
import com.vitorpamplona.amethyst.commons.resources.media_servers_add_section
|
||||
import com.vitorpamplona.amethyst.commons.resources.media_servers_add_url_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.media_servers_cache_section
|
||||
@@ -79,6 +82,7 @@ import com.vitorpamplona.amethyst.commons.resources.media_servers_priority_secti
|
||||
import com.vitorpamplona.amethyst.commons.resources.media_servers_recommended_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.media_servers_reorder_hint
|
||||
import com.vitorpamplona.amethyst.commons.resources.media_servers_upload_section
|
||||
import com.vitorpamplona.amethyst.commons.resources.my_blossom_data
|
||||
import com.vitorpamplona.amethyst.commons.resources.no_blossom_server_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.use_default_servers
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
@@ -250,7 +254,7 @@ private fun UploadBehaviorSection(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(id = R.string.my_blossom_data),
|
||||
text = stringRes(id = Res.string.my_blossom_data),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
@@ -449,7 +453,7 @@ private fun AddServerSection(
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp),
|
||||
)
|
||||
MediaServerEditField(R.string.add_a_blossom_server) { onAddServer(it) }
|
||||
MediaServerEditField(Res.string.add_a_blossom_server) { onAddServer(it) }
|
||||
}
|
||||
|
||||
/** A recommended server as a tappable pill. Once added it reads as done and stops responding. */
|
||||
@@ -611,15 +615,15 @@ private fun HealthIndicator(health: ServerHealth) {
|
||||
when (health) {
|
||||
ServerHealth.Online -> {
|
||||
color = MaterialTheme.colorScheme.allGoodColor
|
||||
label = R.string.media_server_status_online
|
||||
label = Res.string.media_server_status_online
|
||||
}
|
||||
ServerHealth.Slow -> {
|
||||
color = MaterialTheme.colorScheme.warningColor
|
||||
label = R.string.media_server_status_slow
|
||||
label = Res.string.media_server_status_slow
|
||||
}
|
||||
else -> {
|
||||
color = MaterialTheme.colorScheme.error
|
||||
label = R.string.media_server_status_offline
|
||||
label = Res.string.media_server_status_offline
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -47,7 +47,6 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
@@ -55,6 +54,7 @@ import com.vitorpamplona.amethyst.commons.resources.local_blossom_cache_detected
|
||||
import com.vitorpamplona.amethyst.commons.resources.local_blossom_cache_not_detected
|
||||
import com.vitorpamplona.amethyst.commons.resources.local_blossom_cache_profile_pics_only
|
||||
import com.vitorpamplona.amethyst.commons.resources.local_blossom_cache_profile_pics_only_caption
|
||||
import com.vitorpamplona.amethyst.commons.resources.media_servers
|
||||
import com.vitorpamplona.amethyst.commons.resources.use_local_blossom_cache
|
||||
import com.vitorpamplona.amethyst.commons.resources.use_local_blossom_cache_caption
|
||||
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
|
||||
@@ -91,7 +91,7 @@ fun MediaServersScaffold(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBarWithBackButton(
|
||||
caption = stringRes(id = R.string.media_servers),
|
||||
caption = stringRes(id = Res.string.media_servers),
|
||||
nav = nav,
|
||||
)
|
||||
},
|
||||
|
||||
+17
-10
@@ -96,18 +96,21 @@ import coil3.compose.AsyncImage
|
||||
import coil3.compose.AsyncImagePainter
|
||||
import coil3.compose.SubcomposeAsyncImage
|
||||
import coil3.compose.SubcomposeAsyncImageContent
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.back
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_delete_from_host
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_file_details
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_import_menu
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_mirror_to_missing
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_more_actions
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_not_on_all_servers
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_on_all_servers
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_open
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_pay
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_pay_sats
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_payment_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_payment_server_says
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_payment_title
|
||||
@@ -119,8 +122,12 @@ import com.vitorpamplona.amethyst.commons.resources.blossom_send
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_stored_on
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_sync_all
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_sync_gaps
|
||||
import com.vitorpamplona.amethyst.commons.resources.cancel
|
||||
import com.vitorpamplona.amethyst.commons.resources.copy
|
||||
import com.vitorpamplona.amethyst.commons.resources.manage_stored_files_empty
|
||||
import com.vitorpamplona.amethyst.commons.resources.my_blossom_data
|
||||
import com.vitorpamplona.amethyst.commons.resources.quick_action_share
|
||||
import com.vitorpamplona.amethyst.commons.resources.retry
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.VideoViewInner
|
||||
import com.vitorpamplona.amethyst.ui.components.util.setText
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
@@ -197,7 +204,7 @@ fun BlossomBlobManagerScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBarExtensibleWithBackButton(
|
||||
title = { Text(stringRes(R.string.my_blossom_data)) },
|
||||
title = { Text(stringRes(Res.string.my_blossom_data)) },
|
||||
showBackButton = nav.canPop(),
|
||||
popBack = { nav.popBack() },
|
||||
actions = {
|
||||
@@ -233,7 +240,7 @@ fun BlossomBlobManagerScreen(
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(error ?: "", color = MaterialTheme.colorScheme.grayText)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
OutlinedButton(onClick = { vm.refresh() }) { Text(stringRes(R.string.retry)) }
|
||||
OutlinedButton(onClick = { vm.refresh() }) { Text(stringRes(Res.string.retry)) }
|
||||
}
|
||||
|
||||
blobs.isEmpty() ->
|
||||
@@ -479,7 +486,7 @@ private fun SyncBadge(
|
||||
symbol = if (synced) MaterialSymbols.CheckCircle else MaterialSymbols.CloudUpload,
|
||||
contentDescription =
|
||||
stringRes(
|
||||
if (synced) R.string.blossom_on_all_servers else R.string.blossom_not_on_all_servers,
|
||||
if (synced) Res.string.blossom_on_all_servers else Res.string.blossom_not_on_all_servers,
|
||||
),
|
||||
modifier = Modifier.size(13.dp),
|
||||
tint = accent,
|
||||
@@ -554,10 +561,10 @@ private fun BlossomBlobViewer(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
ViewerIconButton(MaterialSymbols.AutoMirrored.ArrowBack, stringRes(R.string.back), onDismiss)
|
||||
ViewerIconButton(MaterialSymbols.AutoMirrored.ArrowBack, stringRes(Res.string.back), onDismiss)
|
||||
Spacer(Modifier.weight(1f))
|
||||
if (url != null) {
|
||||
ViewerIconButton(MaterialSymbols.Share, stringRes(R.string.quick_action_share)) {
|
||||
ViewerIconButton(MaterialSymbols.Share, stringRes(Res.string.quick_action_share)) {
|
||||
shareUrl(context, url)
|
||||
}
|
||||
}
|
||||
@@ -711,7 +718,7 @@ private fun BlobActionsContent(
|
||||
DetailAction(MaterialSymbols.ContentCopy, stringRes(Res.string.copy)) {
|
||||
scope.launch { clipboard.setText(url) }
|
||||
}
|
||||
DetailAction(MaterialSymbols.Share, stringRes(R.string.quick_action_share)) {
|
||||
DetailAction(MaterialSymbols.Share, stringRes(Res.string.quick_action_share)) {
|
||||
shareUrl(context, url)
|
||||
}
|
||||
DetailAction(MaterialSymbols.AutoMirrored.OpenInNew, stringRes(Res.string.blossom_open)) {
|
||||
@@ -852,7 +859,7 @@ private fun BlossomPaymentDialog(
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Text(
|
||||
if (amountSats != null) {
|
||||
pluralStringResource(R.plurals.blossom_pay_sats, amountSats.toInt(), amountSats.toInt())
|
||||
pluralStringResource(Res.plurals.blossom_pay_sats, amountSats.toInt(), amountSats.toInt())
|
||||
} else {
|
||||
stringRes(Res.string.blossom_pay)
|
||||
},
|
||||
@@ -860,7 +867,7 @@ private fun BlossomPaymentDialog(
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text(stringRes(R.string.cancel)) }
|
||||
TextButton(onClick = onDismiss) { Text(stringRes(Res.string.cancel)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -914,7 +921,7 @@ private fun BlossomReportDialog(
|
||||
}) { Text(stringRes(Res.string.blossom_send)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text(stringRes(R.string.cancel)) }
|
||||
TextButton(onClick = onDismiss) { Text(stringRes(Res.string.cancel)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+13
-7
@@ -61,11 +61,16 @@ 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.R
|
||||
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.add_a_blossom_server
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_import_add_url_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_import_busy
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_import_disable_all
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_import_enable_all
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_import_files_found
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_import_found_files
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_import_intro
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_import_manage_servers
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_import_no_targets
|
||||
@@ -74,6 +79,7 @@ import com.vitorpamplona.amethyst.commons.resources.blossom_import_scan
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_import_scanning
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_import_source_failed
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_import_sources_section
|
||||
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.ui.navigation.navs.INav
|
||||
@@ -174,7 +180,7 @@ fun BlossomImportScreen(
|
||||
TextButton(onClick = { vm.setAll(anyDisabled) }, enabled = sources.isNotEmpty()) {
|
||||
Text(
|
||||
stringRes(
|
||||
if (anyDisabled) R.string.blossom_import_enable_all else R.string.blossom_import_disable_all,
|
||||
if (anyDisabled) Res.string.blossom_import_enable_all else Res.string.blossom_import_disable_all,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -196,7 +202,7 @@ fun BlossomImportScreen(
|
||||
color = MaterialTheme.colorScheme.grayText,
|
||||
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp),
|
||||
)
|
||||
MediaServerEditField(R.string.add_a_blossom_server) { vm.addCustom(it) }
|
||||
MediaServerEditField(Res.string.add_a_blossom_server) { vm.addCustom(it) }
|
||||
}
|
||||
|
||||
item {
|
||||
@@ -247,7 +253,7 @@ fun BlossomImportScreen(
|
||||
is ImportStart.Started -> nav.popBack()
|
||||
ImportStart.Busy ->
|
||||
Toast
|
||||
.makeText(context, stringRes(context, R.string.blossom_import_busy), Toast.LENGTH_LONG)
|
||||
.makeText(context, stringRes(Res.string.blossom_import_busy), Toast.LENGTH_LONG)
|
||||
.show()
|
||||
ImportStart.Empty -> {}
|
||||
}
|
||||
@@ -367,7 +373,7 @@ private fun ScanStatusLabel(
|
||||
is SourceScanState.Found -> {
|
||||
val count = scan.count
|
||||
Text(
|
||||
text = pluralStringResource(R.plurals.blossom_import_files_found, count, count),
|
||||
text = pluralStringResource(Res.plurals.blossom_import_files_found, count, count),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (count > 0) MaterialTheme.colorScheme.allGoodColor else MaterialTheme.colorScheme.grayText,
|
||||
)
|
||||
@@ -398,13 +404,13 @@ private fun ImportResultCard(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = pluralStringResource(R.plurals.blossom_import_found_files, count, count),
|
||||
text = pluralStringResource(Res.plurals.blossom_import_found_files, count, count),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
FilledTonalButton(onClick = onImport, modifier = Modifier.fillMaxWidth()) {
|
||||
Icon(symbol = MaterialSymbols.CloudDownload, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Text(pluralStringResource(R.plurals.blossom_import_start_button, count, count))
|
||||
Text(pluralStringResource(Res.plurals.blossom_import_start_button, count, count))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -52,11 +52,11 @@ 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.R
|
||||
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.blossom_sync_done
|
||||
import com.vitorpamplona.amethyst.commons.resources.blossom_syncing
|
||||
import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomSyncState
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
|
||||
@@ -120,7 +120,7 @@ fun DisplayBlossomSyncProgress() {
|
||||
tint = if (s.running) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.allGoodColor,
|
||||
)
|
||||
Text(
|
||||
text = if (s.running) stringRes(R.string.blossom_syncing) else stringRes(Res.string.blossom_sync_done),
|
||||
text = if (s.running) stringRes(Res.string.blossom_syncing) else stringRes(Res.string.blossom_sync_done),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
||||
+4
-2
@@ -22,7 +22,9 @@ package com.vitorpamplona.amethyst.ui.actions.mediaServers
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.file_server
|
||||
import com.vitorpamplona.amethyst.commons.resources.file_server_description
|
||||
import com.vitorpamplona.amethyst.ui.components.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
|
||||
@@ -49,7 +51,7 @@ fun FileServerSelectionRow(
|
||||
?: fileServers.firstOrNull()?.name
|
||||
?: ""
|
||||
|
||||
SettingsRow(R.string.file_server, R.string.file_server_description) {
|
||||
SettingsRow(Res.string.file_server, Res.string.file_server_description) {
|
||||
TextSpinner(
|
||||
label = "",
|
||||
placeholder = placeholder,
|
||||
|
||||
+6
-3
@@ -35,16 +35,19 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.add
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_a_nip96_server
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.references.HttpUrlFormatter
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
|
||||
@Composable
|
||||
fun MediaServerEditField(
|
||||
label: Int = R.string.add_a_nip96_server,
|
||||
label: StringResource = Res.string.add_a_nip96_server,
|
||||
modifier: Modifier = Modifier,
|
||||
onAddServer: (String) -> Unit,
|
||||
) {
|
||||
@@ -88,7 +91,7 @@ fun MediaServerEditField(
|
||||
shape = ButtonBorder,
|
||||
enabled = validUrl,
|
||||
) {
|
||||
Text(text = stringRes(id = R.string.add), color = Color.White)
|
||||
Text(text = stringRes(id = Res.string.add), color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-6
@@ -51,7 +51,6 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
@@ -62,8 +61,13 @@ import com.vitorpamplona.amethyst.commons.resources.nests_servers_add_relay_fiel
|
||||
import com.vitorpamplona.amethyst.commons.resources.nests_servers_auth_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.nests_servers_empty
|
||||
import com.vitorpamplona.amethyst.commons.resources.nests_servers_explainer
|
||||
import com.vitorpamplona.amethyst.commons.resources.nests_servers_my_explainer
|
||||
import com.vitorpamplona.amethyst.commons.resources.nests_servers_my_section
|
||||
import com.vitorpamplona.amethyst.commons.resources.nests_servers_recommended_explainer
|
||||
import com.vitorpamplona.amethyst.commons.resources.nests_servers_recommended_section
|
||||
import com.vitorpamplona.amethyst.commons.resources.nests_servers_relay_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.nests_servers_remove
|
||||
import com.vitorpamplona.amethyst.commons.resources.nests_servers_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.nests_servers_use_defaults
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.AllMediaServersScreen
|
||||
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
|
||||
@@ -121,7 +125,7 @@ private fun NestsServersScaffold(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SavingTopBar(
|
||||
titleRes = R.string.nests_servers_title,
|
||||
titleRes = Res.string.nests_servers_title,
|
||||
onCancel = {
|
||||
viewModel.refresh()
|
||||
onClose()
|
||||
@@ -170,8 +174,8 @@ private fun NestsServersBody(viewModel: NestsServersViewModel) {
|
||||
) {
|
||||
item {
|
||||
SettingsCategory(
|
||||
R.string.nests_servers_my_section,
|
||||
R.string.nests_servers_my_explainer,
|
||||
Res.string.nests_servers_my_section,
|
||||
Res.string.nests_servers_my_explainer,
|
||||
SettingsCategoryFirstModifier,
|
||||
)
|
||||
}
|
||||
@@ -202,8 +206,8 @@ private fun NestsServersBody(viewModel: NestsServersViewModel) {
|
||||
|
||||
item {
|
||||
SettingsCategoryWithButton(
|
||||
title = R.string.nests_servers_recommended_section,
|
||||
description = R.string.nests_servers_recommended_explainer,
|
||||
title = Res.string.nests_servers_recommended_section,
|
||||
description = Res.string.nests_servers_recommended_explainer,
|
||||
modifier = SettingsCategorySpacingModifier,
|
||||
) {
|
||||
OutlinedButton(
|
||||
|
||||
+7
-5
@@ -50,15 +50,17 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.add
|
||||
import com.vitorpamplona.amethyst.commons.resources.delete_payment_target
|
||||
import com.vitorpamplona.amethyst.commons.resources.no_payment_targets_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.payment_target_authority
|
||||
import com.vitorpamplona.amethyst.commons.resources.payment_target_type
|
||||
import com.vitorpamplona.amethyst.commons.resources.payment_targets
|
||||
import com.vitorpamplona.amethyst.commons.resources.payment_targets_explainer
|
||||
import com.vitorpamplona.amethyst.commons.resources.payment_targets_section_explainer
|
||||
import com.vitorpamplona.amethyst.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
|
||||
@@ -100,7 +102,7 @@ fun PaymentTargetsScaffold(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SavingTopBar(
|
||||
titleRes = R.string.payment_targets,
|
||||
titleRes = Res.string.payment_targets,
|
||||
onCancel = {
|
||||
viewModel.refresh()
|
||||
onClose()
|
||||
@@ -150,8 +152,8 @@ fun PaymentTargetsBody(viewModel: PaymentTargetsViewModel) {
|
||||
) {
|
||||
item {
|
||||
SettingsCategory(
|
||||
R.string.payment_targets,
|
||||
R.string.payment_targets_section_explainer,
|
||||
Res.string.payment_targets,
|
||||
Res.string.payment_targets_section_explainer,
|
||||
SettingsCategoryFirstModifier,
|
||||
)
|
||||
}
|
||||
@@ -270,7 +272,7 @@ fun PaymentTargetAddField(onAdd: (type: String, authority: String) -> Unit) {
|
||||
shape = ButtonBorder,
|
||||
enabled = isValid,
|
||||
) {
|
||||
Text(text = stringRes(id = R.string.add), color = Color.White)
|
||||
Text(text = stringRes(id = Res.string.add), color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,10 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.record_a_message_description
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.ui.components.ToggleableBox
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
|
||||
@@ -88,7 +89,7 @@ fun RecordAudioBox(
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
stringRes(context, R.string.record_a_message_description),
|
||||
loadStringRes(Res.string.record_a_message_description),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
|
||||
@@ -46,10 +46,10 @@ import androidx.core.content.FileProvider
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.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.ui.stringRes
|
||||
@@ -162,7 +162,7 @@ private suspend fun ActivityResultLauncher<Uri>.launchOrToast(
|
||||
try {
|
||||
this@launchOrToast.launch(uri)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(context, R.string.no_camera_app_found, Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(context, Res.string.no_camera_app_found, Toast.LENGTH_LONG).show()
|
||||
onUnavailable()
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -48,13 +48,15 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.pause
|
||||
import com.vitorpamplona.amethyst.commons.resources.play
|
||||
import com.vitorpamplona.amethyst.commons.resources.re_record
|
||||
import com.vitorpamplona.amethyst.commons.resources.record_a_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.recording_indicator_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.remove
|
||||
import com.vitorpamplona.amethyst.ui.components.AudioWaveformReadOnly
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta
|
||||
@@ -125,7 +127,7 @@ fun VoiceMessagePreview(
|
||||
) {
|
||||
Icon(
|
||||
symbol = if (isPlaying) MaterialSymbols.Pause else MaterialSymbols.PlayArrow,
|
||||
contentDescription = if (isPlaying) stringRes(context, R.string.pause) else stringRes(context, R.string.play),
|
||||
contentDescription = if (isPlaying) stringRes(Res.string.pause) else stringRes(Res.string.play),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
@@ -168,7 +170,7 @@ fun VoiceMessagePreview(
|
||||
) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Close,
|
||||
contentDescription = stringRes(context, R.string.remove),
|
||||
contentDescription = stringRes(Res.string.remove),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,14 +20,18 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.actions.uploads
|
||||
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.voice_preset_deep
|
||||
import com.vitorpamplona.amethyst.commons.resources.voice_preset_high
|
||||
import com.vitorpamplona.amethyst.commons.resources.voice_preset_neutral
|
||||
import com.vitorpamplona.amethyst.commons.resources.voice_preset_none
|
||||
|
||||
enum class VoicePreset(
|
||||
val pitchFactor: Double,
|
||||
val labelRes: Int,
|
||||
) {
|
||||
NONE(1.0, R.string.voice_preset_none),
|
||||
DEEP(1.4, R.string.voice_preset_deep),
|
||||
HIGH(0.75, R.string.voice_preset_high),
|
||||
NEUTRAL(1.1, R.string.voice_preset_neutral),
|
||||
NONE(1.0, Res.string.voice_preset_none),
|
||||
DEEP(1.4, Res.string.voice_preset_deep),
|
||||
HIGH(0.75, Res.string.voice_preset_high),
|
||||
NEUTRAL(1.1, Res.string.voice_preset_neutral),
|
||||
}
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ fun VoicePresetSelector(
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
} else {
|
||||
Text(stringRes(context, preset.labelRes))
|
||||
Text(stringRes(preset.labelRes))
|
||||
}
|
||||
},
|
||||
colors =
|
||||
|
||||
@@ -66,11 +66,12 @@ import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.defaults.Constants
|
||||
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.bookmarks
|
||||
import com.vitorpamplona.amethyst.commons.resources.boost
|
||||
import com.vitorpamplona.amethyst.commons.resources.bradcasting_result_failure
|
||||
import com.vitorpamplona.amethyst.commons.resources.bradcasting_result_partial
|
||||
import com.vitorpamplona.amethyst.commons.resources.bradcasting_result_success
|
||||
@@ -78,14 +79,23 @@ import com.vitorpamplona.amethyst.commons.resources.broadcasting
|
||||
import com.vitorpamplona.amethyst.commons.resources.broadcasting_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.broadcasting_number_events
|
||||
import com.vitorpamplona.amethyst.commons.resources.event_sent
|
||||
import com.vitorpamplona.amethyst.commons.resources.post
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_cancel_dialog_discard
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_cancel_dialog_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_cancel_dialog_send_without_pow
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_cancel_dialog_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_mining_job
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_mining_progress
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_mining_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_notification_cancel_all
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_queued_job
|
||||
import com.vitorpamplona.amethyst.commons.resources.reaction
|
||||
import com.vitorpamplona.amethyst.commons.resources.retry_failed
|
||||
import com.vitorpamplona.amethyst.commons.resources.sent_number_events
|
||||
import com.vitorpamplona.amethyst.commons.resources.share_of
|
||||
import com.vitorpamplona.amethyst.commons.resources.tap_to_view_details
|
||||
import com.vitorpamplona.amethyst.commons.resources.voice_post
|
||||
import com.vitorpamplona.amethyst.commons.resources.voice_reply
|
||||
import com.vitorpamplona.amethyst.commons.service.broadcast.BroadcastEvent
|
||||
import com.vitorpamplona.amethyst.commons.service.broadcast.BroadcastStatus
|
||||
import com.vitorpamplona.amethyst.commons.service.broadcast.RelayResult
|
||||
@@ -255,13 +265,13 @@ private fun MiningContent(
|
||||
) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Manufacturing,
|
||||
contentDescription = stringRes(R.string.pow_mining_title),
|
||||
contentDescription = stringRes(Res.string.pow_mining_title),
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = gearAlpha),
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = pluralStringResource(R.plurals.pow_mining_progress, miningJobs.size, miningJobs.size),
|
||||
text = pluralStringResource(Res.plurals.pow_mining_progress, miningJobs.size, miningJobs.size),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
@@ -283,7 +293,7 @@ private fun MiningContent(
|
||||
|
||||
val base =
|
||||
pluralStringResource(
|
||||
if (job.isMining) R.plurals.pow_mining_job else R.plurals.pow_queued_job,
|
||||
if (job.isMining) Res.plurals.pow_mining_job else Res.plurals.pow_queued_job,
|
||||
job.difficulty,
|
||||
kindToName(job.kind),
|
||||
job.difficulty,
|
||||
@@ -292,7 +302,7 @@ private fun MiningContent(
|
||||
buildList {
|
||||
elapsedSec?.let { add(DateUtils.formatElapsedTime(it)) }
|
||||
if (elapsedSec != null && expectedSec != null) {
|
||||
add(formatTimeLeft(context, expectedSec, elapsedSec))
|
||||
add(formatTimeLeft(expectedSec, elapsedSec))
|
||||
}
|
||||
}.joinToString(" • ")
|
||||
|
||||
@@ -323,7 +333,7 @@ private fun MiningContent(
|
||||
) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Close,
|
||||
contentDescription = stringRes(R.string.pow_notification_cancel_all),
|
||||
contentDescription = stringRes(Res.string.pow_notification_cancel_all),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(14.dp),
|
||||
)
|
||||
@@ -710,14 +720,14 @@ fun MultipleCompletedBroadcastContent(
|
||||
@Composable
|
||||
fun Event.toKindName(): String =
|
||||
when (this) {
|
||||
is ReactionEvent -> stringRes(R.string.reaction)
|
||||
is RepostEvent -> stringRes(R.string.boost)
|
||||
is GenericRepostEvent -> stringRes(R.string.boost)
|
||||
is VoiceEvent -> stringRes(R.string.voice_post)
|
||||
is VoiceReplyEvent -> stringRes(R.string.voice_reply)
|
||||
is BookmarkListEvent -> stringRes(R.string.bookmarks)
|
||||
is OldBookmarkListEvent -> stringRes(R.string.bookmarks)
|
||||
else -> stringRes(R.string.post)
|
||||
is ReactionEvent -> stringRes(Res.string.reaction)
|
||||
is RepostEvent -> stringRes(Res.string.boost)
|
||||
is GenericRepostEvent -> stringRes(Res.string.boost)
|
||||
is VoiceEvent -> stringRes(Res.string.voice_post)
|
||||
is VoiceReplyEvent -> stringRes(Res.string.voice_reply)
|
||||
is BookmarkListEvent -> stringRes(Res.string.bookmarks)
|
||||
is OldBookmarkListEvent -> stringRes(Res.string.bookmarks)
|
||||
else -> stringRes(Res.string.post)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+4
-3
@@ -68,7 +68,6 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.defaults.Constants
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
@@ -77,6 +76,8 @@ import com.vitorpamplona.amethyst.commons.resources.broadcast_results
|
||||
import com.vitorpamplona.amethyst.commons.resources.broadcasts_number
|
||||
import com.vitorpamplona.amethyst.commons.resources.collapse
|
||||
import com.vitorpamplona.amethyst.commons.resources.dismiss
|
||||
import com.vitorpamplona.amethyst.commons.resources.expand
|
||||
import com.vitorpamplona.amethyst.commons.resources.retry
|
||||
import com.vitorpamplona.amethyst.commons.resources.retry_failed_number
|
||||
import com.vitorpamplona.amethyst.commons.resources.retrying
|
||||
import com.vitorpamplona.amethyst.commons.resources.share_of
|
||||
@@ -258,7 +259,7 @@ private fun BroadcastSection(
|
||||
|
||||
Icon(
|
||||
symbol = if (isExpanded) MaterialSymbols.ExpandLess else MaterialSymbols.ExpandMore,
|
||||
contentDescription = if (isExpanded) stringRes(Res.string.collapse) else stringRes(R.string.expand),
|
||||
contentDescription = if (isExpanded) stringRes(Res.string.collapse) else stringRes(Res.string.expand),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
@@ -443,7 +444,7 @@ private fun RelayResultRow(
|
||||
) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Refresh,
|
||||
contentDescription = stringResource(R.string.retry),
|
||||
contentDescription = stringResource(Res.string.retry),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
|
||||
@@ -41,11 +41,14 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.nipACWebRtcCalls.CallState
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_hangup
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_mute
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_unmute
|
||||
import com.vitorpamplona.amethyst.service.call.CallSessionBridge
|
||||
import com.vitorpamplona.amethyst.service.call.notification.CallNotifier
|
||||
import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.RelayAuthSubscription
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.StringResSetup
|
||||
import com.vitorpamplona.amethyst.ui.call.session.CallSession
|
||||
import com.vitorpamplona.amethyst.ui.screen.ManageRelayServices
|
||||
import com.vitorpamplona.amethyst.ui.screen.ManageWebOkHttp
|
||||
@@ -139,8 +142,6 @@ class CallActivity : AppCompatActivity() {
|
||||
|
||||
setContent {
|
||||
AmethystTheme {
|
||||
StringResSetup()
|
||||
|
||||
// Pauses relay services when the app pauses
|
||||
ManageRelayServices()
|
||||
ManageWebOkHttp()
|
||||
@@ -364,8 +365,8 @@ class CallActivity : AppCompatActivity() {
|
||||
this,
|
||||
if (isMuted) R.drawable.ic_mic_off else R.drawable.ic_mic_on,
|
||||
),
|
||||
getString(if (isMuted) R.string.call_unmute else R.string.call_mute),
|
||||
getString(if (isMuted) R.string.call_unmute else R.string.call_mute),
|
||||
getString(if (isMuted) Res.string.call_unmute else Res.string.call_mute),
|
||||
getString(if (isMuted) Res.string.call_unmute else Res.string.call_mute),
|
||||
muteIntent,
|
||||
)
|
||||
actions.add(muteAction)
|
||||
@@ -381,8 +382,8 @@ class CallActivity : AppCompatActivity() {
|
||||
val hangupAction =
|
||||
RemoteAction(
|
||||
Icon.createWithResource(this, R.drawable.ic_call_end),
|
||||
getString(R.string.call_hangup),
|
||||
getString(R.string.call_hangup),
|
||||
getString(Res.string.call_hangup),
|
||||
getString(Res.string.call_hangup),
|
||||
hangupIntent,
|
||||
)
|
||||
actions.add(hangupAction)
|
||||
|
||||
@@ -38,11 +38,12 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_permission_denied_cancel
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_permission_denied_open_settings
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_permission_denied_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_permission_denied_video
|
||||
import com.vitorpamplona.amethyst.commons.resources.call_permission_denied_voice
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
fun hasPermission(
|
||||
@@ -154,9 +155,9 @@ private fun CallPermissionDeniedDialog(
|
||||
Text(
|
||||
stringRes(
|
||||
if (isVideo) {
|
||||
R.string.call_permission_denied_video
|
||||
Res.string.call_permission_denied_video
|
||||
} else {
|
||||
R.string.call_permission_denied_voice
|
||||
Res.string.call_permission_denied_voice
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user