feat: render group member pictures in call UI, capped at 4 + remaining count
- Add GroupCallPictures composable showing up to 4 user avatars in a grid layout (same pattern as NonClickableUserPictures in chat) - When >4 members, show 3 avatars + a "+N" badge in the 4th slot - Add GroupCallNames composable showing up to 2 names + "+N" remaining - Update CallInProgressUI, IncomingCallUI, ConnectedCallUI, PipCallUI, and PipConnectedCallUI to pass full peerPubKeys/groupMembers sets instead of only .first() https://claude.ai/code/session_01LR8NmFGdMoDTVcfKjL7HWR
This commit is contained in:
@@ -68,6 +68,8 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
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 androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
@@ -76,6 +78,7 @@ import com.vitorpamplona.amethyst.commons.call.CallManager
|
|||||||
import com.vitorpamplona.amethyst.commons.call.CallState
|
import com.vitorpamplona.amethyst.commons.call.CallState
|
||||||
import com.vitorpamplona.amethyst.service.call.AudioRoute
|
import com.vitorpamplona.amethyst.service.call.AudioRoute
|
||||||
import com.vitorpamplona.amethyst.service.call.CallController
|
import com.vitorpamplona.amethyst.service.call.CallController
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
|
||||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
@@ -123,10 +126,10 @@ fun CallScreen(
|
|||||||
|
|
||||||
is CallState.Offering -> {
|
is CallState.Offering -> {
|
||||||
if (isInPipMode) {
|
if (isInPipMode) {
|
||||||
PipCallUI(peerPubKey = state.peerPubKeys.first(), statusText = stringRes(R.string.call_calling), accountViewModel = accountViewModel)
|
PipCallUI(peerPubKeys = state.peerPubKeys, statusText = stringRes(R.string.call_calling), accountViewModel = accountViewModel)
|
||||||
} else {
|
} else {
|
||||||
CallInProgressUI(
|
CallInProgressUI(
|
||||||
peerPubKey = state.peerPubKeys.first(),
|
peerPubKeys = state.peerPubKeys,
|
||||||
statusText = stringRes(R.string.call_calling),
|
statusText = stringRes(R.string.call_calling),
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
onHangup = { scope.launch { callManager.hangup() } },
|
onHangup = { scope.launch { callManager.hangup() } },
|
||||||
@@ -136,7 +139,7 @@ fun CallScreen(
|
|||||||
|
|
||||||
is CallState.IncomingCall -> {
|
is CallState.IncomingCall -> {
|
||||||
if (isInPipMode) {
|
if (isInPipMode) {
|
||||||
PipCallUI(callerPubKey = state.callerPubKey, statusText = stringRes(R.string.call_incoming), accountViewModel = accountViewModel)
|
PipCallUI(peerPubKeys = state.groupMembers, statusText = stringRes(R.string.call_incoming), accountViewModel = accountViewModel)
|
||||||
} else {
|
} else {
|
||||||
val isVideoCall = state.callType == com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallType.VIDEO
|
val isVideoCall = state.callType == com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallType.VIDEO
|
||||||
val acceptWithPermission =
|
val acceptWithPermission =
|
||||||
@@ -144,7 +147,7 @@ fun CallScreen(
|
|||||||
callController?.acceptIncomingCall(state.sdpOffer)
|
callController?.acceptIncomingCall(state.sdpOffer)
|
||||||
}
|
}
|
||||||
IncomingCallUI(
|
IncomingCallUI(
|
||||||
callerPubKey = state.callerPubKey,
|
groupMembers = state.groupMembers,
|
||||||
callType = state.callType,
|
callType = state.callType,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
onAccept = acceptWithPermission,
|
onAccept = acceptWithPermission,
|
||||||
@@ -155,10 +158,10 @@ fun CallScreen(
|
|||||||
|
|
||||||
is CallState.Connecting -> {
|
is CallState.Connecting -> {
|
||||||
if (isInPipMode) {
|
if (isInPipMode) {
|
||||||
PipCallUI(peerPubKey = state.peerPubKeys.first(), statusText = stringRes(R.string.call_connecting), accountViewModel = accountViewModel)
|
PipCallUI(peerPubKeys = state.peerPubKeys, statusText = stringRes(R.string.call_connecting), accountViewModel = accountViewModel)
|
||||||
} else {
|
} else {
|
||||||
CallInProgressUI(
|
CallInProgressUI(
|
||||||
peerPubKey = state.peerPubKeys.first(),
|
peerPubKeys = state.peerPubKeys,
|
||||||
statusText = stringRes(R.string.call_connecting),
|
statusText = stringRes(R.string.call_connecting),
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
onHangup = { scope.launch { callManager.hangup() } },
|
onHangup = { scope.launch { callManager.hangup() } },
|
||||||
@@ -185,7 +188,7 @@ fun CallScreen(
|
|||||||
is CallState.Ended -> {
|
is CallState.Ended -> {
|
||||||
if (!isInPipMode) {
|
if (!isInPipMode) {
|
||||||
CallInProgressUI(
|
CallInProgressUI(
|
||||||
peerPubKey = state.peerPubKeys.first(),
|
peerPubKeys = state.peerPubKeys,
|
||||||
statusText = stringRes(R.string.call_ended),
|
statusText = stringRes(R.string.call_ended),
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
onHangup = { onCallEnded() },
|
onHangup = { onCallEnded() },
|
||||||
@@ -220,7 +223,7 @@ fun CallScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CallInProgressUI(
|
private fun CallInProgressUI(
|
||||||
peerPubKey: String,
|
peerPubKeys: Set<String>,
|
||||||
statusText: String,
|
statusText: String,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
onHangup: () -> Unit,
|
onHangup: () -> Unit,
|
||||||
@@ -238,21 +241,16 @@ private fun CallInProgressUI(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
LoadUser(baseUserHex = peerPubKey, accountViewModel = accountViewModel) { user ->
|
GroupCallPictures(
|
||||||
if (user != null) {
|
peerPubKeys = peerPubKeys,
|
||||||
ClickableUserPicture(
|
size = 120.dp,
|
||||||
baseUser = user,
|
accountViewModel = accountViewModel,
|
||||||
size = 120.dp,
|
)
|
||||||
accountViewModel = accountViewModel,
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
)
|
GroupCallNames(
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
peerPubKeys = peerPubKeys,
|
||||||
UsernameDisplay(
|
accountViewModel = accountViewModel,
|
||||||
baseUser = user,
|
)
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = statusText,
|
text = statusText,
|
||||||
@@ -279,7 +277,7 @@ private fun CallInProgressUI(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun IncomingCallUI(
|
private fun IncomingCallUI(
|
||||||
callerPubKey: String,
|
groupMembers: Set<String>,
|
||||||
callType: com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallType,
|
callType: com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallType,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
onAccept: () -> Unit,
|
onAccept: () -> Unit,
|
||||||
@@ -298,21 +296,16 @@ private fun IncomingCallUI(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
LoadUser(baseUserHex = callerPubKey, accountViewModel = accountViewModel) { user ->
|
GroupCallPictures(
|
||||||
if (user != null) {
|
peerPubKeys = groupMembers,
|
||||||
ClickableUserPicture(
|
size = 120.dp,
|
||||||
baseUser = user,
|
accountViewModel = accountViewModel,
|
||||||
size = 120.dp,
|
)
|
||||||
accountViewModel = accountViewModel,
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
)
|
GroupCallNames(
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
peerPubKeys = groupMembers,
|
||||||
UsernameDisplay(
|
accountViewModel = accountViewModel,
|
||||||
baseUser = user,
|
)
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text =
|
text =
|
||||||
@@ -433,22 +426,17 @@ private fun ConnectedCallUI(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
LoadUser(baseUserHex = state.peerPubKeys.first(), accountViewModel = accountViewModel) { user ->
|
GroupCallPictures(
|
||||||
if (user != null) {
|
peerPubKeys = state.peerPubKeys,
|
||||||
ClickableUserPicture(
|
size = 120.dp,
|
||||||
baseUser = user,
|
accountViewModel = accountViewModel,
|
||||||
size = 120.dp,
|
)
|
||||||
accountViewModel = accountViewModel,
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
)
|
GroupCallNames(
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
peerPubKeys = state.peerPubKeys,
|
||||||
UsernameDisplay(
|
accountViewModel = accountViewModel,
|
||||||
baseUser = user,
|
textColor = Color.White,
|
||||||
accountViewModel = accountViewModel,
|
)
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
textColor = Color.White,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = formatDuration(elapsed),
|
text = formatDuration(elapsed),
|
||||||
@@ -584,12 +572,10 @@ private fun formatDuration(seconds: Long): String {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PipCallUI(
|
private fun PipCallUI(
|
||||||
peerPubKey: String = "",
|
peerPubKeys: Set<String>,
|
||||||
callerPubKey: String = "",
|
|
||||||
statusText: String,
|
statusText: String,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
val pubKey = peerPubKey.ifEmpty { callerPubKey }
|
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
@@ -601,15 +587,11 @@ private fun PipCallUI(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
LoadUser(baseUserHex = pubKey, accountViewModel = accountViewModel) { user ->
|
GroupCallPictures(
|
||||||
if (user != null) {
|
peerPubKeys = peerPubKeys,
|
||||||
ClickableUserPicture(
|
size = 48.dp,
|
||||||
baseUser = user,
|
accountViewModel = accountViewModel,
|
||||||
size = 48.dp,
|
)
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = statusText,
|
text = statusText,
|
||||||
@@ -665,15 +647,11 @@ private fun PipConnectedCallUI(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
LoadUser(baseUserHex = state.peerPubKeys.first(), accountViewModel = accountViewModel) { user ->
|
GroupCallPictures(
|
||||||
if (user != null) {
|
peerPubKeys = state.peerPubKeys,
|
||||||
ClickableUserPicture(
|
size = 48.dp,
|
||||||
baseUser = user,
|
accountViewModel = accountViewModel,
|
||||||
size = 48.dp,
|
)
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = formatDuration(elapsed),
|
text = formatDuration(elapsed),
|
||||||
@@ -696,6 +674,187 @@ private fun PipConnectedCallUI(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun GroupCallPictures(
|
||||||
|
peerPubKeys: Set<String>,
|
||||||
|
size: Dp,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
val userList = remember(peerPubKeys) { peerPubKeys.toList() }
|
||||||
|
val displayCount = minOf(userList.size, 4)
|
||||||
|
val remaining = userList.size - displayCount
|
||||||
|
|
||||||
|
when (userList.size) {
|
||||||
|
0 -> {}
|
||||||
|
|
||||||
|
1 -> {
|
||||||
|
LoadUser(baseUserHex = userList[0], accountViewModel = accountViewModel) { user ->
|
||||||
|
if (user != null) {
|
||||||
|
ClickableUserPicture(
|
||||||
|
baseUser = user,
|
||||||
|
size = size,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Box(Modifier.size(size), contentAlignment = Alignment.TopEnd) {
|
||||||
|
when (displayCount) {
|
||||||
|
2 -> {
|
||||||
|
BaseUserPicture(
|
||||||
|
baseUserHex = userList[0],
|
||||||
|
size = size.div(1.5f),
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
outerModifier = Modifier.size(size.div(1.5f)).align(Alignment.CenterStart),
|
||||||
|
)
|
||||||
|
BaseUserPicture(
|
||||||
|
baseUserHex = userList[1],
|
||||||
|
size = size.div(1.5f),
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
outerModifier = Modifier.size(size.div(1.5f)).align(Alignment.CenterEnd),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
3 -> {
|
||||||
|
BaseUserPicture(
|
||||||
|
baseUserHex = userList[0],
|
||||||
|
size = size.div(1.8f),
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
outerModifier = Modifier.size(size.div(1.8f)).align(Alignment.BottomStart),
|
||||||
|
)
|
||||||
|
BaseUserPicture(
|
||||||
|
baseUserHex = userList[1],
|
||||||
|
size = size.div(1.8f),
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
outerModifier = Modifier.size(size.div(1.8f)).align(Alignment.TopCenter),
|
||||||
|
)
|
||||||
|
BaseUserPicture(
|
||||||
|
baseUserHex = userList[2],
|
||||||
|
size = size.div(1.8f),
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
outerModifier = Modifier.size(size.div(1.8f)).align(Alignment.BottomEnd),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
BaseUserPicture(
|
||||||
|
baseUserHex = userList[0],
|
||||||
|
size = size.div(2f),
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
outerModifier = Modifier.size(size.div(2f)).align(Alignment.BottomStart),
|
||||||
|
)
|
||||||
|
BaseUserPicture(
|
||||||
|
baseUserHex = userList[1],
|
||||||
|
size = size.div(2f),
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
outerModifier = Modifier.size(size.div(2f)).align(Alignment.TopStart),
|
||||||
|
)
|
||||||
|
BaseUserPicture(
|
||||||
|
baseUserHex = userList[2],
|
||||||
|
size = size.div(2f),
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
outerModifier = Modifier.size(size.div(2f)).align(Alignment.BottomEnd),
|
||||||
|
)
|
||||||
|
if (remaining > 0) {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.size(size.div(2f))
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
CircleShape,
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "+$remaining",
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
fontSize = (size.value / 5).sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
BaseUserPicture(
|
||||||
|
baseUserHex = userList[3],
|
||||||
|
size = size.div(2f),
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
outerModifier = Modifier.size(size.div(2f)).align(Alignment.TopEnd),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun GroupCallNames(
|
||||||
|
peerPubKeys: Set<String>,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
textColor: Color = Color.Unspecified,
|
||||||
|
) {
|
||||||
|
val userList = remember(peerPubKeys) { peerPubKeys.toList() }
|
||||||
|
|
||||||
|
when (userList.size) {
|
||||||
|
0 -> {}
|
||||||
|
|
||||||
|
1 -> {
|
||||||
|
LoadUser(baseUserHex = userList[0], accountViewModel = accountViewModel) { user ->
|
||||||
|
if (user != null) {
|
||||||
|
UsernameDisplay(
|
||||||
|
baseUser = user,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textColor = textColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val displayCount = minOf(userList.size, 2)
|
||||||
|
val remaining = userList.size - displayCount
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
for (i in 0 until displayCount) {
|
||||||
|
if (i > 0) {
|
||||||
|
Text(
|
||||||
|
text = ", ",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = textColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
LoadUser(baseUserHex = userList[i], accountViewModel = accountViewModel) { user ->
|
||||||
|
if (user != null) {
|
||||||
|
UsernameDisplay(
|
||||||
|
baseUser = user,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textColor = textColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (remaining > 0) {
|
||||||
|
Text(
|
||||||
|
text = " +$remaining",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = textColor,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun KeepScreenOn() {
|
private fun KeepScreenOn() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|||||||
Reference in New Issue
Block a user