simplify participant data structure

move ParticipantUiState into ParticipantDisplayItem

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2025-05-12 15:01:26 +02:00
parent 962972dce4
commit eaed93087b
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
7 changed files with 49 additions and 87 deletions

View File

@ -48,12 +48,12 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import autodagger.AutoInjector import autodagger.AutoInjector
import com.bluelinelabs.logansquare.LoganSquare import com.bluelinelabs.logansquare.LoganSquare
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -73,7 +73,6 @@ import com.nextcloud.talk.call.MessageSender
import com.nextcloud.talk.call.MessageSenderMcu import com.nextcloud.talk.call.MessageSenderMcu
import com.nextcloud.talk.call.MessageSenderNoMcu import com.nextcloud.talk.call.MessageSenderNoMcu
import com.nextcloud.talk.call.MutableLocalCallParticipantModel import com.nextcloud.talk.call.MutableLocalCallParticipantModel
import com.nextcloud.talk.call.ParticipantUiState
import com.nextcloud.talk.call.ReactionAnimator import com.nextcloud.talk.call.ReactionAnimator
import com.nextcloud.talk.call.components.ParticipantGrid import com.nextcloud.talk.call.components.ParticipantGrid
import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.chat.ChatActivity
@ -154,7 +153,6 @@ import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.launch
import okhttp3.Cache import okhttp3.Cache
import org.apache.commons.lang3.StringEscapeUtils import org.apache.commons.lang3.StringEscapeUtils
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
@ -308,8 +306,6 @@ class CallActivity : CallBaseActivity() {
private var mediaPlayer: MediaPlayer? = null private var mediaPlayer: MediaPlayer? = null
private val participantItems = mutableStateListOf<ParticipantDisplayItem>() private val participantItems = mutableStateListOf<ParticipantDisplayItem>()
private val participantUiStates = mutableStateListOf<ParticipantUiState>()
private var binding: CallActivityBinding? = null private var binding: CallActivityBinding? = null
private var audioOutputDialog: AudioOutputDialog? = null private var audioOutputDialog: AudioOutputDialog? = null
private var moreCallActionsDialog: MoreCallActionsDialog? = null private var moreCallActionsDialog: MoreCallActionsDialog? = null
@ -966,8 +962,9 @@ class CallActivity : CallBaseActivity() {
binding!!.composeParticipantGrid.setContent { binding!!.composeParticipantGrid.setContent {
MaterialTheme { MaterialTheme {
val participantUiStates = participantItems.map { it.uiStateFlow.collectAsState().value }
ParticipantGrid( ParticipantGrid(
participants = participantUiStates.toList(), participantUiStates = participantUiStates,
eglBase = rootEglBase!!, eglBase = rootEglBase!!,
isVoiceOnlyCall = isVoiceOnlyCall isVoiceOnlyCall = isVoiceOnlyCall
) { ) {
@ -2555,7 +2552,6 @@ class CallActivity : CallBaseActivity() {
val participant = participantItems.find { it.sessionKey == key } val participant = participantItems.find { it.sessionKey == key }
participant?.destroy() participant?.destroy()
participantItems.removeAll { it.sessionKey == key } participantItems.removeAll { it.sessionKey == key }
participantUiStates.removeAll { it.sessionKey == key }
initGrid() initGrid()
} }
@ -2674,17 +2670,6 @@ class CallActivity : CallBaseActivity() {
if (participantItems.none { it.sessionKey == sessionKey }) { if (participantItems.none { it.sessionKey == sessionKey }) {
participantItems.add(participantDisplayItem) participantItems.add(participantDisplayItem)
lifecycleScope.launch {
participantDisplayItem.uiStateFlow.collect { uiState ->
val index = participantUiStates.indexOfFirst { it.sessionKey == sessionKey }
if (index >= 0) {
participantUiStates[index] = uiState
} else {
participantUiStates.add(uiState)
}
}
}
} }
initGrid() initGrid()

View File

@ -15,7 +15,6 @@ import android.text.TextUtils
import android.util.Log import android.util.Log
import android.view.ViewGroup import android.view.ViewGroup
import com.nextcloud.talk.call.CallParticipantModel import com.nextcloud.talk.call.CallParticipantModel
import com.nextcloud.talk.call.ParticipantUiState
import com.nextcloud.talk.call.RaisedHand import com.nextcloud.talk.call.RaisedHand
import com.nextcloud.talk.models.json.participants.Participant.ActorType import com.nextcloud.talk.models.json.participants.Participant.ActorType
import com.nextcloud.talk.utils.ApiUtils.getUrlForAvatar import com.nextcloud.talk.utils.ApiUtils.getUrlForAvatar
@ -30,6 +29,17 @@ import org.webrtc.MediaStream
import org.webrtc.PeerConnection.IceConnectionState import org.webrtc.PeerConnection.IceConnectionState
import org.webrtc.SurfaceViewRenderer import org.webrtc.SurfaceViewRenderer
data class ParticipantUiState(
val sessionKey: String,
val nick: String,
val isConnected: Boolean,
val isAudioEnabled: Boolean,
val isStreamEnabled: Boolean,
val raisedHand: Boolean,
val avatarUrl: String?,
val mediaStream: MediaStream?
)
@Suppress("LongParameterList") @Suppress("LongParameterList")
class ParticipantDisplayItem( class ParticipantDisplayItem(
private val context: Context, private val context: Context,

View File

@ -1,21 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.call
import org.webrtc.MediaStream
data class ParticipantUiState(
val sessionKey: String,
val nick: String,
val isConnected: Boolean,
val isAudioEnabled: Boolean,
val isStreamEnabled: Boolean,
val raisedHand: Boolean,
val avatarUrl: String?,
val mediaStream: MediaStream?
)

View File

@ -22,7 +22,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.nextcloud.talk.call.ParticipantUiState import com.nextcloud.talk.adapters.ParticipantUiState
@Composable @Composable
fun AvatarWithFallback(participant: ParticipantUiState, modifier: Modifier = Modifier) { fun AvatarWithFallback(participant: ParticipantUiState, modifier: Modifier = Modifier) {

View File

@ -5,6 +5,8 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
@file:Suppress("MagicNumber", "TooManyFunctions")
package com.nextcloud.talk.call.components package com.nextcloud.talk.call.components
import android.content.res.Configuration import android.content.res.Configuration
@ -23,16 +25,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.nextcloud.talk.call.ParticipantUiState import com.nextcloud.talk.adapters.ParticipantUiState
import org.webrtc.EglBase import org.webrtc.EglBase
import kotlin.math.ceil import kotlin.math.ceil
@Suppress("MagicNumber", "TooManyFunctions")
@Composable @Composable
fun ParticipantGrid( fun ParticipantGrid(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
eglBase: EglBase?, eglBase: EglBase?,
participants: List<ParticipantUiState>, participantUiStates: List<ParticipantUiState>,
isVoiceOnlyCall: Boolean, isVoiceOnlyCall: Boolean,
onClick: () -> Unit onClick: () -> Unit
) { ) {
@ -43,19 +44,19 @@ fun ParticipantGrid(
val columns = val columns =
if (isPortrait) { if (isPortrait) {
when (participants.size) { when (participantUiStates.size) {
1, 2, 3 -> 1 1, 2, 3 -> 1
else -> 2 else -> 2
} }
} else { } else {
when (participants.size) { when (participantUiStates.size) {
1 -> 1 1 -> 1
2, 4 -> 2 2, 4 -> 2
else -> 3 else -> 3
} }
} }
val rows = ceil(participants.size / columns.toFloat()).toInt() val rows = ceil(participantUiStates.size / columns.toFloat()).toInt()
val heightForNonGridComponents = if (isVoiceOnlyCall) { val heightForNonGridComponents = if (isVoiceOnlyCall) {
// this is a workaround for now. It should ~summarize the height of callInfosLinearLayout and callControls // this is a workaround for now. It should ~summarize the height of callInfosLinearLayout and callControls
@ -87,11 +88,11 @@ fun ParticipantGrid(
contentPadding = PaddingValues(vertical = edgePadding) contentPadding = PaddingValues(vertical = edgePadding)
) { ) {
items( items(
participants, participantUiStates,
key = { it.sessionKey } key = { it.sessionKey }
) { participant -> ) { participant ->
ParticipantTile( ParticipantTile(
participant = participant, participantUiState = participant,
modifier = Modifier modifier = Modifier
.height(itemHeight) .height(itemHeight)
.fillMaxWidth(), .fillMaxWidth(),
@ -102,84 +103,76 @@ fun ParticipantGrid(
} }
} }
@Suppress("MagicNumber")
@Preview @Preview
@Composable @Composable
fun ParticipantGridPreview() { fun ParticipantGridPreview() {
ParticipantGrid( ParticipantGrid(
participants = getTestParticipants(1), participantUiStates = getTestParticipants(1),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false
) {} ) {}
} }
@Suppress("MagicNumber")
@Preview @Preview
@Composable @Composable
fun TwoParticipants() { fun TwoParticipants() {
ParticipantGrid( ParticipantGrid(
participants = getTestParticipants(2), participantUiStates = getTestParticipants(2),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false
) {} ) {}
} }
@Suppress("MagicNumber")
@Preview @Preview
@Composable @Composable
fun ThreeParticipants() { fun ThreeParticipants() {
ParticipantGrid( ParticipantGrid(
participants = getTestParticipants(3), participantUiStates = getTestParticipants(3),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false
) {} ) {}
} }
@Suppress("MagicNumber")
@Preview @Preview
@Composable @Composable
fun FourParticipants() { fun FourParticipants() {
ParticipantGrid( ParticipantGrid(
participants = getTestParticipants(4), participantUiStates = getTestParticipants(4),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false
) {} ) {}
} }
@Suppress("MagicNumber")
@Preview @Preview
@Composable @Composable
fun FiveParticipants() { fun FiveParticipants() {
ParticipantGrid( ParticipantGrid(
participants = getTestParticipants(5), participantUiStates = getTestParticipants(5),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false
) {} ) {}
} }
@Suppress("MagicNumber")
@Preview @Preview
@Composable @Composable
fun SevenParticipants() { fun SevenParticipants() {
ParticipantGrid( ParticipantGrid(
participants = getTestParticipants(7), participantUiStates = getTestParticipants(7),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false
) {} ) {}
} }
@Suppress("MagicNumber")
@Preview @Preview
@Composable @Composable
fun FiftyParticipants() { fun FiftyParticipants() {
ParticipantGrid( ParticipantGrid(
participants = getTestParticipants(50), participantUiStates = getTestParticipants(50),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false
) {} ) {}
} }
@Suppress("MagicNumber")
@Preview( @Preview(
showBackground = false, showBackground = false,
heightDp = 360, heightDp = 360,
@ -188,13 +181,12 @@ fun FiftyParticipants() {
@Composable @Composable
fun OneParticipantLandscape() { fun OneParticipantLandscape() {
ParticipantGrid( ParticipantGrid(
participants = getTestParticipants(1), participantUiStates = getTestParticipants(1),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false
) {} ) {}
} }
@Suppress("MagicNumber")
@Preview( @Preview(
showBackground = false, showBackground = false,
heightDp = 360, heightDp = 360,
@ -203,13 +195,12 @@ fun OneParticipantLandscape() {
@Composable @Composable
fun TwoParticipantsLandscape() { fun TwoParticipantsLandscape() {
ParticipantGrid( ParticipantGrid(
participants = getTestParticipants(2), participantUiStates = getTestParticipants(2),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false
) {} ) {}
} }
@Suppress("MagicNumber")
@Preview( @Preview(
showBackground = false, showBackground = false,
heightDp = 360, heightDp = 360,
@ -218,13 +209,12 @@ fun TwoParticipantsLandscape() {
@Composable @Composable
fun ThreeParticipantsLandscape() { fun ThreeParticipantsLandscape() {
ParticipantGrid( ParticipantGrid(
participants = getTestParticipants(3), participantUiStates = getTestParticipants(3),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false
) {} ) {}
} }
@Suppress("MagicNumber")
@Preview( @Preview(
showBackground = false, showBackground = false,
heightDp = 360, heightDp = 360,
@ -233,13 +223,12 @@ fun ThreeParticipantsLandscape() {
@Composable @Composable
fun FourParticipantsLandscape() { fun FourParticipantsLandscape() {
ParticipantGrid( ParticipantGrid(
participants = getTestParticipants(4), participantUiStates = getTestParticipants(4),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false
) {} ) {}
} }
@Suppress("MagicNumber")
@Preview( @Preview(
showBackground = false, showBackground = false,
heightDp = 360, heightDp = 360,
@ -248,13 +237,12 @@ fun FourParticipantsLandscape() {
@Composable @Composable
fun SevenParticipantsLandscape() { fun SevenParticipantsLandscape() {
ParticipantGrid( ParticipantGrid(
participants = getTestParticipants(7), participantUiStates = getTestParticipants(7),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false
) {} ) {}
} }
@Suppress("MagicNumber")
@Preview( @Preview(
showBackground = false, showBackground = false,
heightDp = 360, heightDp = 360,
@ -263,7 +251,7 @@ fun SevenParticipantsLandscape() {
@Composable @Composable
fun FiftyParticipantsLandscape() { fun FiftyParticipantsLandscape() {
ParticipantGrid( ParticipantGrid(
participants = getTestParticipants(50), participantUiStates = getTestParticipants(50),
eglBase = null, eglBase = null,
isVoiceOnlyCall = false isVoiceOnlyCall = false
) {} ) {}

View File

@ -29,7 +29,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.call.ParticipantUiState import com.nextcloud.talk.adapters.ParticipantUiState
import com.nextcloud.talk.utils.ColorGenerator import com.nextcloud.talk.utils.ColorGenerator
import org.webrtc.EglBase import org.webrtc.EglBase
@ -38,28 +38,28 @@ const val NICK_BLUR_RADIUS = 4f
@Composable @Composable
fun ParticipantTile( fun ParticipantTile(
participant: ParticipantUiState, participantUiState: ParticipantUiState,
eglBase: EglBase?, eglBase: EglBase?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isVoiceOnlyCall: Boolean isVoiceOnlyCall: Boolean
) { ) {
val colorInt = ColorGenerator.shared.usernameToColor(participant.nick) val colorInt = ColorGenerator.shared.usernameToColor(participantUiState.nick)
Box( Box(
modifier = modifier modifier = modifier
.clip(RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(12.dp))
.background(Color(colorInt)) .background(Color(colorInt))
) { ) {
if (!isVoiceOnlyCall && participant.isStreamEnabled && participant.mediaStream != null) { if (!isVoiceOnlyCall && participantUiState.isStreamEnabled && participantUiState.mediaStream != null) {
WebRTCVideoView(participant, eglBase) WebRTCVideoView(participantUiState, eglBase)
} else { } else {
AvatarWithFallback( AvatarWithFallback(
participant = participant, participant = participantUiState,
modifier = Modifier.align(Alignment.Center) modifier = Modifier.align(Alignment.Center)
) )
} }
if (participant.raisedHand) { if (participantUiState.raisedHand) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_hand_back_left), painter = painterResource(id = R.drawable.ic_hand_back_left),
contentDescription = "Raised Hand", contentDescription = "Raised Hand",
@ -71,7 +71,7 @@ fun ParticipantTile(
) )
} }
if (!participant.isAudioEnabled) { if (!participantUiState.isAudioEnabled) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_mic_off_white_24px), painter = painterResource(id = R.drawable.ic_mic_off_white_24px),
contentDescription = "Mic Off", contentDescription = "Mic Off",
@ -84,7 +84,7 @@ fun ParticipantTile(
} }
Text( Text(
text = participant.nick, text = participantUiState.nick,
color = Color.White, color = Color.White,
modifier = Modifier modifier = Modifier
.align(Alignment.BottomStart) .align(Alignment.BottomStart)
@ -98,7 +98,7 @@ fun ParticipantTile(
) )
) )
if (!participant.isConnected) { if (!participantUiState.isConnected) {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center) modifier = Modifier.align(Alignment.Center)
) )
@ -120,7 +120,7 @@ fun ParticipantTilePreview() {
mediaStream = null mediaStream = null
) )
ParticipantTile( ParticipantTile(
participant = participant, participantUiState = participant,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(300.dp), .height(300.dp),

View File

@ -11,7 +11,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import com.nextcloud.talk.call.ParticipantUiState import com.nextcloud.talk.adapters.ParticipantUiState
import org.webrtc.EglBase import org.webrtc.EglBase
import org.webrtc.SurfaceViewRenderer import org.webrtc.SurfaceViewRenderer