From bc53577df5b32a1f5096da86fc578bf8d785e37e Mon Sep 17 00:00:00 2001 From: Taksh Date: Mon, 5 Oct 2026 16:10:58 +0530 Subject: [PATCH 1/3] Add localized message accessibility descriptions Split the existing PR change into a smaller reviewable piece. Original change: a11y: label delivery status and announce incoming messages --- app/src/main/res/values/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a46d596f3..8aaff4845 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -79,6 +79,14 @@ Add bookmark + Message sending + Message sent + Message delivered + Message read + Message failed to send + Message sending, %1$d of %2$d parts delivered + New message from %1$s: %2$s + Voice message Nostr reachable Messages Conversations From e106050beea24e9358f991c4298ff147e944230e Mon Sep 17 00:00:00 2001 From: Taksh Date: Mon, 5 Oct 2026 16:10:58 +0530 Subject: [PATCH 2/3] Expose delivery status to TalkBack Split the existing PR change into a smaller reviewable piece. Original change: a11y: label delivery status and announce incoming messages --- .../bitchat/android/ui/MessageComponents.kt | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt b/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt index 8eb76c44e..296f037e4 100644 --- a/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt @@ -1231,8 +1231,20 @@ private fun deliveryCheckRank(status: DeliveryStatus): Int = when (status) { else -> 0 } +@Composable +private fun deliveryStatusDescription(status: DeliveryStatus): String = when (status) { + DeliveryStatus.Sending -> stringResource(R.string.cd_delivery_sending) + DeliveryStatus.Sent -> stringResource(R.string.cd_delivery_sent) + is DeliveryStatus.Delivered -> stringResource(R.string.cd_delivery_delivered) + is DeliveryStatus.Read -> stringResource(R.string.cd_delivery_read) + is DeliveryStatus.Failed -> stringResource(R.string.cd_delivery_failed) + is DeliveryStatus.PartiallyDelivered -> + stringResource(R.string.cd_delivery_partial, status.reached, status.total) +} + @Composable fun DeliveryStatusIcon(status: DeliveryStatus) { + val statusDescription = deliveryStatusDescription(status) val colorScheme = MaterialTheme.colorScheme val (firstTarget, secondTarget) = deliveryCheckColors(status, colorScheme) val first by animateColorAsState( @@ -1270,9 +1282,11 @@ fun DeliveryStatusIcon(status: DeliveryStatus) { text = text, fontSize = 10.sp, fontWeight = FontWeight.Normal, - modifier = Modifier.graphicsLayer { - scaleX = scale.value - scaleY = scale.value - } + modifier = Modifier + .semantics { contentDescription = statusDescription } + .graphicsLayer { + scaleX = scale.value + scaleY = scale.value + }, ) } From c6183bac3a0e3ebca6fa49df75c9eb79485ff226 Mon Sep 17 00:00:00 2001 From: Taksh Date: Mon, 5 Oct 2026 16:10:58 +0530 Subject: [PATCH 3/3] Announce incoming messages through a polite live region Split the existing PR change into a smaller reviewable piece. Original change: a11y: label delivery status and announce incoming messages --- .../bitchat/android/ui/MessageComponents.kt | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt b/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt index 296f037e4..2265c2189 100644 --- a/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt @@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -53,6 +54,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable @@ -69,7 +71,9 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.LiveRegionMode import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.liveRegion import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextLayoutResult @@ -293,6 +297,13 @@ fun MessagesList( arrivalTracker.arrivals(messages) } + IncomingMessageAccessibilityAnnouncer( + messages = messages, + enteringIds = enteringIds, + currentUserNickname = currentUserNickname, + enabled = followIncomingMessages, + ) + // Placement animation exists to soften insertions and removals. But *any* relayout moves every // item — the keyboard opening behind a bottom sheet, that sheet closing again, the composer // growing a line — and animating those made the whole conversation lurch. So it is armed only @@ -1242,6 +1253,53 @@ private fun deliveryStatusDescription(status: DeliveryStatus): String = when (st stringResource(R.string.cd_delivery_partial, status.reached, status.total) } +@Composable +private fun IncomingMessageAccessibilityAnnouncer( + messages: List, + enteringIds: Set, + currentUserNickname: String, + enabled: Boolean, +) { + val context = LocalContext.current + val incomingArrivals = remember(messages, enteringIds) { + messages.filter { it.id in enteringIds && it.sender != currentUserNickname } + } + var announcementVersion by remember { mutableIntStateOf(0) } + LaunchedEffect(incomingArrivals.map { it.id }, enabled) { + if (!enabled || incomingArrivals.isEmpty()) return@LaunchedEffect + announcementVersion++ + } + val announcement = remember(announcementVersion, incomingArrivals) { + if (announcementVersion == 0 || incomingArrivals.isEmpty()) { + return@remember "" + } + val message = incomingArrivals.maxByOrNull { it.timestamp.time } ?: return@remember "" + val preview = messagePreviewForAccessibility(context, message) + context.getString(R.string.a11y_incoming_message, message.sender, preview) + } + if (announcement.isNotEmpty()) { + Box( + Modifier + .size(1.dp) + .semantics { + liveRegion = LiveRegionMode.Polite + contentDescription = announcement + }, + ) + } +} + +private fun messagePreviewForAccessibility(context: Context, message: BitchatMessage): String { + return when (message.type) { + BitchatMessageType.Message -> + message.content.replace('\n', ' ').trim().take(120) + BitchatMessageType.Audio -> + context.getString(R.string.a11y_incoming_voice_message) + BitchatMessageType.Image -> context.getString(R.string.cd_image) + BitchatMessageType.File -> context.getString(R.string.cd_file) + }.ifBlank { "…" } +} + @Composable fun DeliveryStatusIcon(status: DeliveryStatus) { val statusDescription = deliveryStatusDescription(status)