@@ -48,6 +48,7 @@ import androidx.compose.material.icons.Icons
4848import androidx.compose.material.icons.filled.Close
4949import androidx.compose.material.icons.filled.MoreVert
5050import androidx.compose.material.icons.filled.People
51+ import androidx.compose.material.icons.filled.SignalWifiBad
5152import androidx.compose.material.rememberModalBottomSheetState
5253import androidx.compose.runtime.Composable
5354import androidx.compose.runtime.LaunchedEffect
@@ -62,19 +63,23 @@ import androidx.compose.runtime.rememberUpdatedState
6263import androidx.compose.runtime.saveable.rememberSaveable
6364import androidx.compose.runtime.setValue
6465import androidx.compose.ui.Alignment
66+ import androidx.compose.ui.Alignment.Companion.CenterVertically
6567import androidx.compose.ui.Modifier
6668import androidx.compose.ui.draw.clip
6769import androidx.compose.ui.graphics.Color
6870import androidx.compose.ui.graphics.vector.ImageVector
6971import androidx.compose.ui.platform.LocalConfiguration
7072import androidx.compose.ui.platform.LocalContext
7173import androidx.compose.ui.platform.testTag
74+ import androidx.compose.ui.res.stringResource
7275import androidx.compose.ui.res.vectorResource
7376import androidx.compose.ui.state.ToggleableState
77+ import androidx.compose.ui.text.style.TextAlign
7478import androidx.compose.ui.tooling.preview.Preview
7579import androidx.compose.ui.unit.IntOffset
7680import androidx.compose.ui.unit.IntSize
7781import androidx.compose.ui.unit.dp
82+ import androidx.compose.ui.unit.sp
7883import androidx.compose.ui.window.Popup
7984import androidx.lifecycle.compose.collectAsStateWithLifecycle
8085import io.getstream.android.video.generated.models.TranscriptionSettingsResponse
@@ -123,11 +128,15 @@ import io.getstream.video.android.ui.menu.SettingsMenu
123128import io.getstream.video.android.ui.menu.VideoFilter
124129import io.getstream.video.android.ui.menu.availableVideoFilters
125130import io.getstream.video.android.util.config.AppConfig
131+ import kotlinx.coroutines.FlowPreview
126132import kotlinx.coroutines.delay
127133import kotlinx.coroutines.flow.collectLatest
134+ import kotlinx.coroutines.flow.distinctUntilChanged
128135import kotlinx.coroutines.flow.map
136+ import kotlinx.coroutines.flow.sample
129137import kotlinx.coroutines.launch
130138
139+ @OptIn(FlowPreview ::class )
131140@Composable
132141fun CallScreen (
133142 call : Call ,
@@ -174,6 +183,23 @@ fun CallScreen(
174183
175184 val connection by call.state.connection.collectAsStateWithLifecycle()
176185 val me by call.state.me.collectAsStateWithLifecycle()
186+ var anyPausedVideos by remember { mutableStateOf(false ) }
187+
188+ LaunchedEffect (key1 = call) {
189+ while (true ) {
190+ delay(2000 )
191+ call.state.participants
192+ .sample(2000L )
193+ .map { participants ->
194+ // Compute only paused videos
195+ participants.filter { it.videoPaused.value }
196+ }
197+ .distinctUntilChanged() // Only emit if the filtered list actually changes
198+ .collect { pausedVideos ->
199+ anyPausedVideos = pausedVideos.isNotEmpty()
200+ }
201+ }
202+ }
177203
178204 LaunchedEffect (key1 = connection) {
179205 if (connection == RealtimeConnection .Disconnected ) {
@@ -567,7 +593,8 @@ fun CallScreen(
567593 val isPortrait = configuration.orientation == Configuration .ORIENTATION_PORTRAIT
568594
569595 val popupYOffset = if (isPortrait) {
570- - (VideoTheme .dimens.componentHeightL + VideoTheme .dimens.spacingS).toPx().toInt()
596+ - (VideoTheme .dimens.componentHeightL + VideoTheme .dimens.spacingS).toPx()
597+ .toInt()
571598 } else {
572599 0
573600 }
@@ -610,6 +637,10 @@ fun CallScreen(
610637 SpeakingWhileMuted ()
611638 }
612639
640+ if (anyPausedVideos) {
641+ BadNetworkLabel ()
642+ }
643+
613644 if (showingLandscapeControls && orientation == Configuration .ORIENTATION_LANDSCAPE ) {
614645 LandscapeControls (call, onChat = {
615646 showingLandscapeControls = false
@@ -800,6 +831,44 @@ private fun SpeakingWhileMuted() {
800831 }
801832}
802833
834+ @Composable
835+ private fun BadNetworkLabel (
836+ modifier : Modifier = Modifier ,
837+ ) {
838+ Box (modifier = modifier.fillMaxSize()) {
839+ Row (
840+ modifier = modifier
841+ .align(Alignment .BottomCenter )
842+ .padding(vertical = 90 .dp, horizontal = 16 .dp)
843+ .background(
844+ color = VideoTheme .colors.baseSheetQuarternary,
845+ shape = VideoTheme .shapes.sheet,
846+ )
847+ .testTag(" video_renderer_fallback_bad_network" ),
848+ horizontalArrangement = Arrangement .Center ,
849+ verticalAlignment = Alignment .CenterVertically ,
850+ ) {
851+ Icon (
852+ modifier = Modifier
853+ .padding(12 .dp)
854+ .align(CenterVertically ),
855+ imageVector = Icons .Default .SignalWifiBad ,
856+ contentDescription = null ,
857+ tint = VideoTheme .colors.basePrimary,
858+ )
859+ Text (
860+ modifier = Modifier .padding(12 .dp),
861+ text = stringResource(
862+ id = io.getstream.video.android.ui.common.R .string.stream_video_call_bad_network,
863+ ),
864+ color = VideoTheme .colors.basePrimary,
865+ textAlign = TextAlign .Center ,
866+ fontSize = 14 .sp,
867+ )
868+ }
869+ }
870+ }
871+
803872@Composable
804873fun isTablet (): Boolean {
805874 val configuration = LocalConfiguration .current
0 commit comments