Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
918fc16
feat: make bottom calling controls a draggable bottom sheet [WPB-1057]
saleniuk Apr 23, 2026
7a4958d
Merge remote-tracking branch 'origin/develop' into feat/calling-contr…
saleniuk Apr 23, 2026
a2885bf
add scrim to CommonTopAppBar as well
saleniuk Apr 24, 2026
75392d0
detekt
saleniuk Apr 24, 2026
0a48531
Merge remote-tracking branch 'origin/develop' into feat/calling-contr…
saleniuk Apr 24, 2026
89b2a24
disable StatusBarScrim for previews as it breaks them
saleniuk Apr 24, 2026
f350349
Merge remote-tracking branch 'origin/develop' into feat/calling-contr…
saleniuk Apr 27, 2026
bae099d
feat: list of call participants [WPB-1057]
saleniuk Apr 27, 2026
343af8f
detekt
saleniuk Apr 27, 2026
6a25744
Merge remote-tracking branch 'origin/feat/calling-controls-as-bottom-…
saleniuk Apr 27, 2026
50be055
set max width for the calling controls panel
saleniuk Apr 29, 2026
13c14c9
Merge remote-tracking branch 'origin/develop' into feat/calling-contr…
saleniuk Apr 29, 2026
c1c9a5b
resolved comments
saleniuk Apr 30, 2026
8410ead
Merge remote-tracking branch 'origin/develop' into feat/calling-contr…
saleniuk Apr 30, 2026
b3bb64d
Merge remote-tracking branch 'origin/feat/calling-controls-as-bottom-…
saleniuk Apr 30, 2026
eedea57
detekt
saleniuk Apr 30, 2026
174f490
Merge remote-tracking branch 'origin/develop' into feat/call-particip…
saleniuk Apr 30, 2026
a80a2ad
change content description for camera
saleniuk May 4, 2026
aaeee74
Merge remote-tracking branch 'origin/develop' into feat/call-particip…
saleniuk May 4, 2026
0d94a5a
Merge remote-tracking branch 'origin/develop' into feat/call-particip…
saleniuk May 4, 2026
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 @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of of simulating sticky header, can use LazyColum with stickyHeader

 LazyColumn {
    stickyHeader {
        SectionHeader(...)
    }
    items(...) { ... }
}

Copy link
Copy Markdown
Contributor Author

@saleniuk saleniuk May 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not simulating the sticky header, it's actually a different intention than a sticky header. It handles the visuals of the whole list scroll, so it cannot be a part of the list as a sticky header, it needs to stay above the list, so in column it will be drawn first, that's why zIndex is used so that the background of the list doesn't cover the shadow elevation that this element creates.

) {
SectionHeader(name = stringResource(R.string.calling_details_participants_header, participants.size))
}
ParticipantList(
lazyListState = lazyListState,
participants = participants,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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()

Check warning on line 142 in app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantslist/ParticipantItem.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantslist/ParticipantItem.kt#L142

Added line #L142 was not covered by tests

@PreviewMultipleThemes
@Composable
fun PreviewParticipantItem_Muted() = WireTheme {

Check warning on line 146 in app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantslist/ParticipantItem.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename function "PreviewParticipantItem_Muted" to match the regular expression ^[a-zA-Z][a-zA-Z0-9]*$

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-android&issues=AZ3PohqyqgvhdYcrrwyo&open=AZ3PohqyqgvhdYcrrwyo&pullRequest=4762
ParticipantItem(participant = previewParticipant.copy(isMuted = true))
}

@PreviewMultipleThemes
@Composable
fun PreviewParticipantItem_NotMuted() = WireTheme {

Check warning on line 152 in app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantslist/ParticipantItem.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename function "PreviewParticipantItem_NotMuted" to match the regular expression ^[a-zA-Z][a-zA-Z0-9]*$

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-android&issues=AZ3PohqyqgvhdYcrrwyp&open=AZ3PohqyqgvhdYcrrwyp&pullRequest=4762
ParticipantItem(participant = previewParticipant.copy(isMuted = false))
}

@PreviewMultipleThemes
@Composable
fun PreviewParticipantItem_Speaking() = WireTheme {

Check warning on line 158 in app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantslist/ParticipantItem.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename function "PreviewParticipantItem_Speaking" to match the regular expression ^[a-zA-Z][a-zA-Z0-9]*$

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-android&issues=AZ3PohqyqgvhdYcrrwyq&open=AZ3PohqyqgvhdYcrrwyq&pullRequest=4762
ParticipantItem(participant = previewParticipant.copy(isMuted = false, isSpeaking = true))
}

@PreviewMultipleThemes
@Composable
fun PreviewParticipantItem_NotMutedWithCamera() = WireTheme {

Check warning on line 164 in app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantslist/ParticipantItem.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename function "PreviewParticipantItem_NotMutedWithCamera" to match the regular expression ^[a-zA-Z][a-zA-Z0-9]*$

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-android&issues=AZ3PohqyqgvhdYcrrwyr&open=AZ3PohqyqgvhdYcrrwyr&pullRequest=4762
ParticipantItem(participant = previewParticipant.copy(isMuted = false, isCameraOn = true))
}

@PreviewMultipleThemes
@Composable
fun PreviewParticipantItem_NotMutedWithScreenShare() = WireTheme {

Check warning on line 170 in app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantslist/ParticipantItem.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename function "PreviewParticipantItem_NotMutedWithScreenShare" to match the regular expression ^[a-zA-Z][a-zA-Z0-9]*$

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-android&issues=AZ3PohqyqgvhdYcrrwys&open=AZ3PohqyqgvhdYcrrwys&pullRequest=4762
ParticipantItem(participant = previewParticipant.copy(isMuted = false, isSharingScreen = true))
}

@PreviewMultipleThemes
@Composable
fun PreviewParticipantItem_MutedGuest() = WireTheme {

Check warning on line 176 in app/src/main/kotlin/com/wire/android/ui/calling/ongoing/participantslist/ParticipantItem.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename function "PreviewParticipantItem_MutedGuest" to match the regular expression ^[a-zA-Z][a-zA-Z0-9]*$

See more on https://sonarcloud.io/project/issues?id=wireapp_wire-android&issues=AZ3PohqyqgvhdYcrrwyt&open=AZ3PohqyqgvhdYcrrwyt&pullRequest=4762
ParticipantItem(participant = previewParticipant.copy(isMuted = true, membership = Membership.Guest))
}
Original file line number Diff line number Diff line change
@@ -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<UICallParticipant>,
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))
}
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/ic_screen_share.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M12,13V15H4V13H12ZM14.124,1C14.608,1 15,1.379 15,1.845V11.155C15,11.622 14.611,12 14.124,12H9V8H13L8,3L3,8H7L6.999,12H1.876C1.392,12 1,11.621 1,11.155V1.845C1,1.378 1.389,1 1.876,1H14.124Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>
6 changes: 6 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@
<string name="content_description_calling_turn_camera_off">Turn camera off</string>
<string name="content_description_calling_turn_speaker_on">Turn speaker on</string>
<string name="content_description_calling_turn_speaker_off">Turn speaker off</string>
<string name="content_description_calling_screen_share_on">Screen share on</string>
<string name="content_description_calling_camera_on">Camera on</string>
<string name="content_description_calling_microphone_on">Microphone on</string>
<string name="content_description_calling_microphone_off">Microphone off</string>
<string name="content_description_calling_active_speaker">Active speaker</string>
<string name="content_description_calling_in_call_reactions_show">Show in call reactions panel</string>
<string name="content_description_calling_in_call_reactions_hide">Hide in call reactions panel</string>
<string name="content_description_call_open_calling_details">Open calling details</string>
Expand Down Expand Up @@ -1158,6 +1163,7 @@
<string name="calling_details_network_quality_jitter">Jitter</string>
<string name="calling_details_network_quality_value_milliseconds">%1$d ms</string>
<string name="calling_details_network_quality_learn_more">Learn more about the quality details</string>
<string name="calling_details_participants_header">PARTICIPANTS (%d)</string>

<!-- Connectivity Status Bar -->
<string name="connectivity_status_bar_return_to_call">Return to call</string>
Expand Down
Loading