diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/model/UICallParticipant.kt b/app/src/main/kotlin/com/wire/android/ui/calling/model/UICallParticipant.kt index b7b3fcf2407..b4f8ec042cb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/model/UICallParticipant.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/model/UICallParticipant.kt @@ -18,10 +18,12 @@ package com.wire.android.ui.calling.model +import androidx.compose.runtime.Stable import com.wire.android.model.ImageAsset import com.wire.android.ui.home.conversationslist.model.Membership import com.wire.kalium.logic.data.id.QualifiedID +@Stable data class UICallParticipant( val id: QualifiedID, val clientId: String, diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt index 17deddca7c7..377512fe77e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt @@ -23,22 +23,27 @@ package com.wire.android.ui.calling.ongoing import android.content.pm.PackageManager import android.view.View import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.BottomSheetScaffoldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SheetValue +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.rememberStandardBottomSheetState @@ -63,6 +68,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.max +import androidx.compose.ui.zIndex import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver @@ -94,6 +101,7 @@ import com.wire.android.ui.calling.ongoing.incallreactions.InCallReactionsState import com.wire.android.ui.calling.ongoing.incallreactions.PreviewInCallReactionState import com.wire.android.ui.calling.ongoing.incallreactions.drawInCallReactions import com.wire.android.ui.calling.ongoing.incallreactions.rememberInCallReactionsState +import com.wire.android.ui.calling.ongoing.participantslist.ParticipantList import com.wire.android.ui.calling.ongoing.participantsview.FloatingSelfUserTile import com.wire.android.ui.calling.ongoing.participantsview.VerticalCallingPager import com.wire.android.ui.calling.ongoing.toast.InCallToast @@ -111,6 +119,8 @@ import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dialogs.PermissionPermanentlyDeniedDialog import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.progress.WireCircularProgressIndicator +import com.wire.android.ui.common.rememberTopBarElevationState +import com.wire.android.ui.common.rowitem.SectionHeader import com.wire.android.ui.common.topappbar.NavigationIconType import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.visbility.rememberVisibilityState @@ -137,7 +147,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import java.util.Locale -@Suppress("ParameterWrapping") +@Suppress("ParameterWrapping", "CyclomaticComplexMethod") @Composable fun OngoingCallScreen( conversationId: ConversationId, @@ -161,8 +171,9 @@ fun OngoingCallScreen( var inPictureInPictureMode by remember { mutableStateOf(shouldUsePiPMode && activity.isInPictureInPictureMode) } val sheetState = rememberStandardBottomSheetState( initialValue = SheetValue.PartiallyExpanded, - confirmValueChange = { - false // TODO: to be enabled when the participants list is implemented and added to the bottom sheet + confirmValueChange = { targetValue -> + // do not allow to expand the sheet if there is nothing more to show in the expanded state + !(targetValue == SheetValue.Expanded && ongoingCallViewModel.state.participants.isEmpty()) } ) val scaffoldState = rememberBottomSheetScaffoldState(bottomSheetState = sheetState) @@ -463,14 +474,26 @@ private fun OngoingCallContent( } ) BoxWithConstraints { + val navBarHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() Column( modifier = Modifier .heightIn(max = with(LocalDensity.current) { (constraints.maxHeight - topBarHeight).toDp() }) + .padding(top = max(dimensions().spacing8x, navBarHeight)) + .background(colorsScheme().background) ) { - Box( - modifier = Modifier // TODO: replace with proper list of participants + val lazyListState = rememberLazyListState() + Surface( + shadowElevation = lazyListState.rememberTopBarElevationState().value, + color = MaterialTheme.wireColorScheme.background, + modifier = Modifier .fillMaxWidth() - .height(0.dp) + .zIndex(1f) // ensure the section header is above the participant items when scrolled + ) { + SectionHeader(name = stringResource(R.string.calling_details_participants_header, participants.size)) + } + ParticipantList( + lazyListState = lazyListState, + participants = participants, ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantslist/ParticipantItem.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantslist/ParticipantItem.kt new file mode 100644 index 00000000000..b6275bbc821 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantslist/ParticipantItem.kt @@ -0,0 +1,178 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.calling.ongoing.participantslist + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.wire.android.R +import com.wire.android.model.NameBasedAvatar +import com.wire.android.model.UserAvatarData +import com.wire.android.ui.calling.model.UICallParticipant +import com.wire.android.ui.calling.ongoing.buildPreviewParticipantsList +import com.wire.android.ui.common.MembershipQualifierLabel +import com.wire.android.ui.common.avatar.UserProfileAvatar +import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.rowitem.RowItemTemplate +import com.wire.android.ui.common.typography +import com.wire.android.ui.home.conversationslist.model.Membership +import com.wire.android.ui.theme.WireTheme +import com.wire.android.util.ui.PreviewMultipleThemes + +@Composable +fun ParticipantItem( + participant: UICallParticipant, + modifier: Modifier = Modifier, +) { + RowItemTemplate( + leadingIcon = { + UserProfileAvatar( + UserAvatarData(asset = participant.avatar, nameBasedAvatar = NameBasedAvatar(participant.name, participant.accentId)), + ) + }, + titleStartPadding = dimensions().spacing0x, + title = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(dimensions().spacing6x) + ) { + Text( + text = participant.name.orEmpty(), + style = typography().title02, + color = colorsScheme().onSurface, + ) + MembershipQualifierLabel(membership = participant.membership) + } + }, + actions = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(dimensions().spacing8x) + ) { + if (participant.isSharingScreen) { + ActionIcon( + icon = R.drawable.ic_screen_share, + contentDescription = R.string.content_description_calling_screen_share_on, + ) + } + if (participant.isCameraOn) { + ActionIcon( + icon = R.drawable.ic_camera_on, + contentDescription = R.string.content_description_calling_camera_on, + ) + } + if (participant.isMuted) { + ActionIcon( + icon = R.drawable.ic_microphone_off, + contentDescription = R.string.content_description_calling_microphone_off, + ) + } else { + ActionIcon( + icon = R.drawable.ic_microphone_on, + contentDescription = R.string.content_description_calling_microphone_on, + active = participant.isSpeaking, + ) + } + } + }, + modifier = modifier.padding(start = dimensions().spacing8x), + ) +} + +@Composable +private fun ActionIcon( + @DrawableRes icon: Int, + @StringRes contentDescription: Int, + modifier: Modifier = Modifier, + active: Boolean = false, +) { + Icon( + painter = painterResource(icon), + contentDescription = stringResource(contentDescription), + tint = if (active) colorsScheme().primary else colorsScheme().onSurface, + modifier = modifier + .let { + if (active) { + it + .border( + color = colorsScheme().primary, + width = dimensions().spacing1x, + shape = RoundedCornerShape(dimensions().spacing3x) + ) + .background( + color = colorsScheme().primaryVariant, + shape = RoundedCornerShape(dimensions().spacing3x) + ) + } else { + it + } + } + .padding(dimensions().spacing3x) + ) +} + +private val previewParticipant = buildPreviewParticipantsList(1).first() + +@PreviewMultipleThemes +@Composable +fun PreviewParticipantItem_Muted() = WireTheme { + ParticipantItem(participant = previewParticipant.copy(isMuted = true)) +} + +@PreviewMultipleThemes +@Composable +fun PreviewParticipantItem_NotMuted() = WireTheme { + ParticipantItem(participant = previewParticipant.copy(isMuted = false)) +} + +@PreviewMultipleThemes +@Composable +fun PreviewParticipantItem_Speaking() = WireTheme { + ParticipantItem(participant = previewParticipant.copy(isMuted = false, isSpeaking = true)) +} + +@PreviewMultipleThemes +@Composable +fun PreviewParticipantItem_NotMutedWithCamera() = WireTheme { + ParticipantItem(participant = previewParticipant.copy(isMuted = false, isCameraOn = true)) +} + +@PreviewMultipleThemes +@Composable +fun PreviewParticipantItem_NotMutedWithScreenShare() = WireTheme { + ParticipantItem(participant = previewParticipant.copy(isMuted = false, isSharingScreen = true)) +} + +@PreviewMultipleThemes +@Composable +fun PreviewParticipantItem_MutedGuest() = WireTheme { + ParticipantItem(participant = previewParticipant.copy(isMuted = true, membership = Membership.Guest)) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantslist/ParticipantList.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantslist/ParticipantList.kt new file mode 100644 index 00000000000..7f839b06bf1 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantslist/ParticipantList.kt @@ -0,0 +1,55 @@ +/* + * Wire + * Copyright (C) 2026 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.calling.ongoing.participantslist + +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.wire.android.ui.calling.model.UICallParticipant +import com.wire.android.ui.calling.ongoing.buildPreviewParticipantsList +import com.wire.android.ui.theme.WireTheme +import com.wire.android.util.ui.PreviewMultipleThemes +import kotlinx.collections.immutable.PersistentList + +@Composable +fun ParticipantList( + participants: PersistentList, + modifier: Modifier = Modifier, + lazyListState: LazyListState = rememberLazyListState(), +) { + LazyColumn( + state = lazyListState, + modifier = modifier, + ) { + items( + items = participants, + key = { it.id.value + it.clientId }, + ) { participant -> + ParticipantItem(participant) + } + } +} + +@PreviewMultipleThemes +@Composable +fun PreviewParticipantList() = WireTheme { + ParticipantList(participants = buildPreviewParticipantsList(3)) +} diff --git a/app/src/main/res/drawable/ic_screen_share.xml b/app/src/main/res/drawable/ic_screen_share.xml new file mode 100644 index 00000000000..40f8d44f098 --- /dev/null +++ b/app/src/main/res/drawable/ic_screen_share.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 715f68b1087..72ec3acfd25 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -159,6 +159,11 @@ Turn camera off Turn speaker on Turn speaker off + Screen share on + Camera on + Microphone on + Microphone off + Active speaker Show in call reactions panel Hide in call reactions panel Open calling details @@ -1158,6 +1163,7 @@ Jitter %1$d ms Learn more about the quality details + PARTICIPANTS (%d) Return to call