Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2030,7 +2030,6 @@ public final class io/getstream/chat/android/compose/ui/messages/composer/intern
public final fun getLambda$-129303699$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2;
public final fun getLambda$-1855012696$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2;
public final fun getLambda$-2139230466$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2;
public final fun getLambda$-630641788$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2;
public final fun getLambda$491551885$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2;
public final fun getLambda$545156019$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
Expand Down Expand Up @@ -283,6 +284,13 @@ internal fun PlaybackToggleButton(
} else {
painterResource(id = R.drawable.stream_design_ic_play_fill)
}
val label = stringResource(
if (playing) {
R.string.stream_compose_audio_recording_pause
} else {
R.string.stream_compose_audio_recording_play
},
)

StreamButton(
onClick = onClick,
Expand All @@ -293,7 +301,7 @@ internal fun PlaybackToggleButton(
Icon(
painter = icon,
modifier = Modifier.size(20.dp),
contentDescription = null,
contentDescription = label,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
Expand Down Expand Up @@ -125,12 +127,14 @@ internal fun MessageComposerQuotedMessage(
message = message,
currentUser = currentUser,
replyMessage = null,
modifier = Modifier.semantics(mergeDescendants = true) {},
)

ComposerCancelIcon(
modifier = Modifier
.align(Alignment.TopEnd)
.offset(StreamTokens.spacing2xs, -StreamTokens.spacing2xs),
contentDescription = stringResource(R.string.stream_compose_message_composer_cancel_reply),
onClick = onCancelClick,
)
}
Expand All @@ -143,15 +147,25 @@ private fun QuotedMessageUserName(
currentUser: User?,
color: Color,
) {
val userName = if (message.isMine(currentUser)) {
stringResource(R.string.stream_compose_quoted_message_you)
} else if (replyMessage == null) {
stringResource(R.string.stream_compose_quoted_message_reply_to, message.user.name)
val isMine = message.isMine(currentUser)
val isComposerBanner = replyMessage == null
val userName = when {
isMine -> stringResource(R.string.stream_compose_quoted_message_you)
isComposerBanner -> stringResource(R.string.stream_compose_quoted_message_reply_to, message.user.name)
else -> message.user.name
}
val accessibilityName = if (isMine && isComposerBanner) {
stringResource(R.string.stream_compose_quoted_message_reply_to_you)
} else {
message.user.name
null
}

Text(
modifier = if (accessibilityName != null) {
Modifier.semantics { contentDescription = accessibilityName }
} else {
Modifier
},
text = userName,
style = ChatTheme.typography.metadataEmphasis,
color = color,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.chat.android.compose.ui.messages.composer.internal

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import io.getstream.chat.android.compose.R
import io.getstream.chat.android.ui.common.state.messages.composer.RecordingState

/**
* Sets the localized content description (which includes the press-and-hold gesture hint) and
* tags the node as a button so TalkBack announces "Button".
*
* The actual recording is started by the press-and-hold gesture on `pointerInput`; lifecycle
* announcements ("Recording started" / "Recording cancelled") are emitted by
* [AnnounceRecordingTransitions], not by this modifier.
*/
@Composable
internal fun Modifier.micButtonSemantics(): Modifier {
val buttonDescription = stringResource(R.string.stream_compose_audio_recording_start)
return this.semantics {
contentDescription = buttonDescription
role = Role.Button
}
}

/**
* Observes [recordingState] transitions and emits one-shot TalkBack announcements:
* - `Idle → Hold` announces "Recording started"
* - `non-Idle → Idle` announces "Recording cancelled" only when [cancelRequested] is `true`
*
* The caller flips [cancelRequested] on when the user invokes a cancel or delete action; this
* distinguishes a real cancellation from the confirm / send paths, which also transition back
* to `Idle` but emit through a brief `Complete` state that Compose batches away.
*
* @param recordingState Current recording state to observe.
* @param cancelRequested `true` when the user invoked a cancel or delete action since the last
* transition was processed.
* @param onTransitionConsumed Invoked only when a transition is actually announced, so the
* caller can reset [cancelRequested] to `false`. Transitions that don't result in an
* announcement leave the flag untouched.
*/
@Composable
internal fun AnnounceRecordingTransitions(
recordingState: RecordingState,
cancelRequested: Boolean,
onTransitionConsumed: () -> Unit,
) {
val view = LocalView.current
val startedAnnouncement = stringResource(R.string.stream_compose_audio_recording_state_started)
val cancelledAnnouncement = stringResource(R.string.stream_compose_audio_recording_state_cancelled)
var previousWasIdle by remember { mutableStateOf(true) }
val currentIsIdle = recordingState is RecordingState.Idle
val currentIsHold = recordingState is RecordingState.Hold

LaunchedEffect(currentIsIdle, currentIsHold) {
val wasIdle = previousWasIdle
previousWasIdle = currentIsIdle
var consumed = false
when (recordingState) {
is RecordingState.Hold -> if (wasIdle) {
view.announceForAccessibility(startedAnnouncement)
consumed = true
}
is RecordingState.Idle -> if (!wasIdle && cancelRequested) {
view.announceForAccessibility(cancelledAnnouncement)
consumed = true
}
is RecordingState.Locked,
is RecordingState.Overview,
is RecordingState.Complete,
-> Unit
}
if (consumed) onTransitionConsumed()
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,6 @@ import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
Expand Down Expand Up @@ -118,11 +116,31 @@ internal fun AudioRecordingButton(
val showFloatingIcons = recordingState is RecordingState.Hold || recordingState is RecordingState.Locked
val floatingMic = rememberFloatingMicState(recordingState)

var cancelRequested by remember { mutableStateOf(false) }
val cancelTrackingActions = remember(recordingActions) {
recordingActions.copy(
onCancelRecording = {
cancelRequested = true
recordingActions.onCancelRecording()
},
onDeleteRecording = {
cancelRequested = true
recordingActions.onDeleteRecording()
},
)
}

AnnounceRecordingTransitions(
recordingState = recordingState,
cancelRequested = cancelRequested,
onTransitionConsumed = { cancelRequested = false },
)

Box(modifier = modifier.applyIf(isRecording) { fillMaxWidth() }) {
if (isRecording) {
AudioRecordingContent(
recordingState = recordingState,
recordingActions = recordingActions,
recordingActions = cancelTrackingActions,
modifier = Modifier.fillMaxWidth(),
)
}
Expand All @@ -131,7 +149,7 @@ internal fun AudioRecordingButton(
modifier = Modifier.align(Alignment.CenterEnd),
isVisible = floatingMic.isVisible,
recordingState = recordingState,
recordingActions = recordingActions,
recordingActions = cancelTrackingActions,
floatingActive = floatingMic.isFloating,
floatingOffset = floatingMic.offset,
)
Expand Down Expand Up @@ -310,13 +328,11 @@ private fun MicButtonGestureArea(
val showPressed = isFingerDown || hint.snackbarHostState.currentSnackbarData != null
PressInteractionEffect(showPressed, pressOffset, interactionSource)

val buttonDescription = stringResource(R.string.stream_compose_audio_recording_start)

val buttonSize = if (isVisible) MicButtonSize else 0.dp
Box(
modifier = Modifier
.size(buttonSize)
.semantics { contentDescription = buttonDescription }
.micButtonSemantics()
.pointerInput(Unit) {
awaitEachGesture {
val down = awaitFirstDown()
Expand Down
Loading
Loading