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:
Claude
2026-09-22 15:06:49 +00:00
parent d0f85b1a92
commit a33942f2a2
652 changed files with 88066 additions and 85229 deletions
@@ -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
}
@@ -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))
@@ -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 {
@@ -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,
)
@@ -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,
)
}
@@ -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 =
@@ -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) },
)
@@ -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
}
@@ -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
@@ -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,
)
}
@@ -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!!)
@@ -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,
)
@@ -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()
}
}
@@ -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())
}
@@ -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),
)
}
}
@@ -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 = {
@@ -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)
},
@@ -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(),
@@ -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)
},
)
@@ -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) {
@@ -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)
}
}
@@ -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
@@ -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)
@@ -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) },
@@ -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
@@ -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
},
),
)
@@ -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())
@@ -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
}
@@ -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(),
@@ -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,
@@ -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
@@ -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,
@@ -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(),
@@ -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,
@@ -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(),
@@ -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(),
@@ -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
@@ -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,
@@ -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()
},
@@ -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(),
@@ -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
}
@@ -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))
}
}
}
@@ -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),
)
}
@@ -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,
)
@@ -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()
@@ -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),
)
}
}
@@ -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,
)
}
@@ -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),
)
}
}
@@ -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 }
@@ -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))
@@ -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
}
@@ -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 =
@@ -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
}
@@ -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)
}
/**
@@ -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,
@@ -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,
)
}
@@ -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(),
)
@@ -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!!)
@@ -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)
}
@@ -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 {
@@ -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))
}
}
}
@@ -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)
}
}
@@ -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()
@@ -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 },
@@ -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
@@ -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))
}
},
)
@@ -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)
}
}
}
@@ -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
}
}
@@ -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,
)
},
@@ -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)) }
},
)
}
@@ -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))
}
}
}
@@ -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,
@@ -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,
@@ -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)
}
}
}
@@ -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(
@@ -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()
}
}
@@ -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),
}
@@ -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
@@ -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