Code review:

- @Volatile ScheduledPostNotifier.channel: two workers firing in the
  same window can race on ensureChannel from different IO threads.
  createNotificationChannel is idempotent, but the field write needs a
  visibility fence so both threads see the cached reference.
- @Volatile PoolEventOutbox.eventOutbox map ref + PoolEventOutboxState
  .relaysRemaining set ref. The new pendingPublishRelaysFor polling
  path reads these from the WorkManager IO thread; mutations still
  happen on NostrClient's IO scope. Pre-existing visibility gap that
  this poll surface exposed; @Volatile is the minimal fix.
- ScheduledPostsScreen.SectionLabel: read the locale from
  LocalConfiguration.current.locales[0] (the Compose-resolved locale)
  rather than Locale.getDefault() (the system locale, which can drift
  from app config and breaks Turkish I/i casing).
This commit is contained in:
davotoula
2026-05-07 20:52:52 +02:00
parent c9f6762751
commit 474a957f2b
4 changed files with 13 additions and 3 deletions
@@ -39,6 +39,10 @@ import com.vitorpamplona.amethyst.ui.stringRes
* now guards against; the notification closes the loop. * now guards against; the notification closes the loop.
*/ */
object ScheduledPostNotifier { object ScheduledPostNotifier {
// @Volatile so the channel reference is visible across the WorkManager IO
// thread pool — two workers firing in the same window can race on
// ensureChannel. createNotificationChannel itself is idempotent.
@Volatile
private var channel: NotificationChannel? = null private var channel: NotificationChannel? = null
private const val SCHEDULED_POST_NOT_ID_BASE = 0x70000 private const val SCHEDULED_POST_NOT_ID_BASE = 0x70000
@@ -78,6 +78,7 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
@@ -111,7 +112,6 @@ import java.time.LocalDate
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle import java.time.format.FormatStyle
import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable @Composable
@@ -460,8 +460,9 @@ private fun ScheduledPostCardExpandedPanel(
@Composable @Composable
private fun SectionLabel(text: String) { private fun SectionLabel(text: String) {
val locale = LocalConfiguration.current.locales[0]
Text( Text(
text = text.uppercase(Locale.getDefault()), text = text.uppercase(locale),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 4.dp), modifier = Modifier.padding(bottom = 4.dp),
@@ -31,6 +31,11 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
class PoolEventOutbox { class PoolEventOutbox {
// @Volatile so the polling path (INostrClient.pendingPublishRelaysFor)
// sees current state from threads that didn't write the map. Mutations
// still happen on NostrClient's IO scope; this only closes the
// visibility gap for cross-thread readers.
@Volatile
private var eventOutbox = mapOf<HexKey, PoolEventOutboxState>() private var eventOutbox = mapOf<HexKey, PoolEventOutboxState>()
val relays = MutableStateFlow(setOf<NormalizedRelayUrl>()) val relays = MutableStateFlow(setOf<NormalizedRelayUrl>())
@@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils
class PoolEventOutboxState( class PoolEventOutboxState(
val event: Event, val event: Event,
var relaysRemaining: Set<NormalizedRelayUrl>, @Volatile var relaysRemaining: Set<NormalizedRelayUrl>,
) { ) {
private var failures = mapOf<NormalizedRelayUrl, Tries>() private var failures = mapOf<NormalizedRelayUrl, Tries>()