diff --git a/stream-chat-android-compose-sample/src/main/AndroidManifest.xml b/stream-chat-android-compose-sample/src/main/AndroidManifest.xml index 11f4624330d..be45f938777 100644 --- a/stream-chat-android-compose-sample/src/main/AndroidManifest.xml +++ b/stream-chat-android-compose-sample/src/main/AndroidManifest.xml @@ -66,54 +66,71 @@ - - + + Failed to load more files attachments + Channel Open channel info + + Profile + diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/AddMembersScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/AddMembersScreen.kt index de7958f87ce..329deed70af 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/AddMembersScreen.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/AddMembersScreen.kt @@ -40,6 +40,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.paneTitle +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -101,11 +103,13 @@ internal fun AddMembersScreen( onDismiss: () -> Unit, onConfirm: () -> Unit, ) { + val paneTitleText = stringResource(id = R.string.stream_compose_add_members_title) Column( modifier = Modifier .fillMaxSize() .background(ChatTheme.colors.backgroundCoreApp) - .systemBarsPadding(), + .systemBarsPadding() + .semantics { paneTitle = paneTitleText }, ) { AddMembersHeader( hasSelection = state.selectedUserIds.isNotEmpty(), diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/DirectChannelInfoScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/DirectChannelInfoScreen.kt index 834b01f9ef5..7e6db0ae4e8 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/DirectChannelInfoScreen.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/DirectChannelInfoScreen.kt @@ -42,6 +42,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.paneTitle +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -113,8 +115,9 @@ private fun DirectChannelInfoScaffold( onViewAction: (action: ChannelInfoViewAction) -> Unit = {}, ) { val listState = rememberLazyListState() + val paneTitleText = stringResource(id = UiCommonR.string.stream_ui_channel_info_contact_title) Scaffold( - modifier = modifier, + modifier = modifier.semantics { paneTitle = paneTitleText }, topBar = { ChatTheme.componentFactory.DirectChannelInfoTopBar( params = DirectChannelInfoTopBarParams( diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/GroupChannelEditScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/GroupChannelEditScreen.kt index 431cded5720..e07642174b4 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/GroupChannelEditScreen.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/GroupChannelEditScreen.kt @@ -54,6 +54,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.paneTitle +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextOverflow @@ -206,7 +208,9 @@ private fun GroupChannelEditContent( onSaveActionClick: () -> Unit = {}, onUploadPictureClick: () -> Unit = {}, ) { + val paneTitleText = stringResource(id = UiCommonR.string.stream_ui_channel_info_edit_title) Scaffold( + modifier = Modifier.semantics { paneTitle = paneTitleText }, topBar = { GroupChannelEditTopBar( isBusy = isBusy, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/GroupChannelInfoScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/GroupChannelInfoScreen.kt index 84aef692257..4a4f67df612 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/GroupChannelInfoScreen.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/GroupChannelInfoScreen.kt @@ -50,6 +50,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.paneTitle +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -174,8 +176,9 @@ private fun GroupChannelInfoScaffold( onViewAction: (action: ChannelInfoViewAction) -> Unit = {}, ) { val listState = rememberLazyListState() + val paneTitleText = stringResource(id = UiCommonR.string.stream_ui_channel_info_group_title) Scaffold( - modifier = modifier, + modifier = modifier.semantics { paneTitle = paneTitleText }, topBar = { ChatTheme.componentFactory.GroupChannelInfoTopBar( params = GroupChannelInfoTopBarParams( diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/ChannelsScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/ChannelsScreen.kt index daf295912f3..f0796be33b9 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/ChannelsScreen.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/ChannelsScreen.kt @@ -41,6 +41,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.paneTitle +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import io.getstream.chat.android.compose.state.channels.list.SearchQuery @@ -67,7 +69,8 @@ import io.getstream.chat.android.ui.common.state.channels.actions.ViewInfo * You can use the default implementation by not passing in an instance yourself, or you * can customize the behavior using its parameters. * @param viewModelKey Key to differentiate between instances of [ChannelListViewModel]. - * @param title Header title. + * @param title Header title. Also drives the screen's `paneTitle` semantic, announced by TalkBack + * when the screen appears as a pane (e.g. an adaptive-layout pane or a Compose Navigation route). * @param isShowingHeader If we show the header or hide it. * @param searchMode The search mode for the screen. * @param onHeaderActionClick Handler for the default header action. @@ -123,7 +126,9 @@ public fun ChannelsScreen( .testTag("Stream_ChannelsScreen"), ) { Scaffold( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .semantics { paneTitle = title }, topBar = { if (isShowingHeader) { ChatTheme.componentFactory.ChannelListHeader( diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/chats/ChatsScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/chats/ChatsScreen.kt index d5e1ad9b649..3e3b1b47b23 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/chats/ChatsScreen.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/chats/ChatsScreen.kt @@ -44,6 +44,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.paneTitle +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.lifecycle.viewmodel.compose.viewModel @@ -106,6 +108,8 @@ import kotlin.math.abs * When the initial [ChannelViewModelFactory] is requested (before a channel is selected), * `channelId`, `messageId`, and `parentMessageId` are `null`. * @param title The title displayed in the list pane top bar. Default is `"Stream Chat"`. + * Also drives the list pane's `paneTitle` semantic, announced by TalkBack when the list pane + * appears or becomes active (e.g. switching between panes in the adaptive layout). * @param searchMode The current search mode. Default is [SearchMode.None]. * @param listContentMode The mode for displaying the list content. Default is [ChatListContentMode.Channels]. * @param onBackPress Callback invoked when the user presses the back button. @@ -179,10 +183,12 @@ public fun ChatsScreen( onDispose { navigator.popUpTo(ThreePaneRole.List) } } - val listPane = remember(listContentMode) { + val listPane = remember(listContentMode, title) { movableContentOf { modifier: Modifier -> Scaffold( - modifier = modifier.safeDrawingPadding(), + modifier = modifier + .safeDrawingPadding() + .semantics { paneTitle = title }, containerColor = ChatTheme.colors.backgroundCoreApp, topBar = { listTopBarContent() }, bottomBar = { listBottomBarContent() }, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadsScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadsScreen.kt index 0992eb5c278..86f8babeb17 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadsScreen.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadsScreen.kt @@ -23,6 +23,8 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.paneTitle +import androidx.compose.ui.semantics.semantics import androidx.lifecycle.viewmodel.compose.viewModel import io.getstream.chat.android.client.api.models.QueryThreadsRequest import io.getstream.chat.android.compose.R @@ -38,7 +40,8 @@ import io.getstream.chat.android.models.Thread * It can be used without most parameters for default behavior, that can be tweaked if necessary. * * @param viewModelFactory The factory used to build the [ThreadListViewModel]. - * @param title Header title. + * @param title Header title. Also drives the screen's `paneTitle` semantic, announced by TalkBack + * when the screen appears as a pane (e.g. an adaptive-layout pane or a Compose Navigation route). * @param onHeaderAvatarClick Handle for when the user clicks on the header avatar. * @param onThreadClick Handler for Thread item clicks. */ @@ -54,7 +57,7 @@ public fun ThreadsScreen( val user by listViewModel.user.collectAsState() val connectionState by listViewModel.connectionState.collectAsState() - Column(modifier = Modifier.fillMaxSize()) { + Column(modifier = Modifier.fillMaxSize().semantics { paneTitle = title }) { ChatTheme.componentFactory.ThreadListHeader( params = ThreadListHeaderParams( title = title, diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/chats/ChatsScreenTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/chats/ChatsScreenTest.kt index 48a49ebdadb..3f6fc817d4a 100644 --- a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/chats/ChatsScreenTest.kt +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/chats/ChatsScreenTest.kt @@ -88,4 +88,16 @@ internal class ChatsScreenTest : MockedChatClientTest { composeTestRule.onNodeWithTag("Stream_ThreadListLoading") .assertExists() } + + @Test + @UiThread + fun `with custom title`() { + composeTestRule.setContent { + ChatTheme { + ChatsScreen(title = "My Chats") + } + } + + composeTestRule.onNodeWithText("My Chats").assertExists() + } } diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/threads/ThreadsScreenTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/threads/ThreadsScreenTest.kt new file mode 100644 index 00000000000..41554f124a8 --- /dev/null +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/threads/ThreadsScreenTest.kt @@ -0,0 +1,74 @@ +/* + * 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.threads + +import androidx.annotation.UiThread +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.getstream.chat.android.client.test.MockedChatClientTest +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.models.ConnectionState +import io.getstream.chat.android.randomUser +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [33]) +internal class ThreadsScreenTest : MockedChatClientTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Before + fun prepare() { + whenever(mockClientState.user) doReturn MutableStateFlow(randomUser()) + whenever(mockClientState.connectionState) doReturn MutableStateFlow(ConnectionState.Connected) + } + + @Test + @UiThread + fun `with default title`() { + composeTestRule.setContent { + ChatTheme { + ThreadsScreen() + } + } + + composeTestRule.onNodeWithText("Threads").assertExists() + composeTestRule.onNodeWithTag("Stream_ThreadListLoading").assertExists() + } + + @Test + @UiThread + fun `with custom title`() { + composeTestRule.setContent { + ChatTheme { + ThreadsScreen(title = "My Threads") + } + } + + composeTestRule.onNodeWithText("My Threads").assertExists() + } +}