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:
+4
@@ -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
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -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),
|
||||||
|
|||||||
+5
@@ -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>())
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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>()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user