diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallOverflowPopupWindow.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallOverflowPopupWindow.kt index 6c72155f2b..bcf383e2f4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallOverflowPopupWindow.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallOverflowPopupWindow.kt @@ -10,7 +10,9 @@ import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.LinearLayout import android.widget.PopupWindow +import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.widget.PopupWindowCompat import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.WebRtcCallActivity @@ -22,15 +24,21 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies */ class CallOverflowPopupWindow(private val activity: WebRtcCallActivity, parentViewGroup: ViewGroup) : PopupWindow( LayoutInflater.from(activity).inflate(R.layout.call_overflow_holder, parentViewGroup, false), - activity.resources.getDimension(R.dimen.reaction_scrubber_width).toInt(), - ViewGroup.LayoutParams.WRAP_CONTENT + activity.resources.getDimension(R.dimen.calling_reaction_popup_menu_width).toInt(), + activity.resources.getDimension(R.dimen.calling_reaction_popup_menu_height).toInt() + ) { init { - (contentView as CallReactionScrubber).initialize(activity.supportFragmentManager, activity) { + val root = (contentView as LinearLayout) + root.findViewById(R.id.reaction_scrubber).initialize(activity.supportFragmentManager) { ApplicationDependencies.getSignalCallManager().react(it) dismiss() } + root.findViewById(R.id.raise_hand_layout_parent).setOnClickListener { + ApplicationDependencies.getSignalCallManager().raiseHand(true) + dismiss() + } } fun show(anchor: View) { @@ -45,7 +53,7 @@ class CallOverflowPopupWindow(private val activity: WebRtcCallActivity, parentVi val windowWidth = windowRect.width() val popupWidth = resources.getDimension(R.dimen.reaction_scrubber_width).toInt() - val popupHeight = resources.getDimension(R.dimen.calling_reaction_emoji_height).toInt() + val popupHeight = resources.getDimension(R.dimen.calling_reaction_popup_menu_height).toInt() val xOffset = windowWidth - popupWidth - margin val yOffset = -popupHeight - margin diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.kt index 8605ed8f27..27de25b216 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.kt @@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.webrtc.WebRtcControls.FoldableState import org.thoughtcrime.securesms.events.CallParticipant import org.thoughtcrime.securesms.events.CallParticipant.Companion.createLocal +import org.thoughtcrime.securesms.events.GroupCallRaiseHandEvent import org.thoughtcrime.securesms.events.GroupCallReactionEvent import org.thoughtcrime.securesms.events.WebRtcViewModel import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry @@ -26,10 +27,11 @@ data class CallParticipantsState( val callState: WebRtcViewModel.State = WebRtcViewModel.State.CALL_DISCONNECTED, val groupCallState: WebRtcViewModel.GroupCallState = WebRtcViewModel.GroupCallState.IDLE, private val remoteParticipants: ParticipantCollection = ParticipantCollection(SMALL_GROUP_MAX), - val localParticipant: CallParticipant = createLocal(CameraState.UNKNOWN, BroadcastVideoSink(), false), + val localParticipant: CallParticipant = createLocal(CameraState.UNKNOWN, BroadcastVideoSink(), microphoneEnabled = false, isHandRaised = false), val focusedParticipant: CallParticipant = CallParticipant.EMPTY, val localRenderState: WebRtcLocalRenderState = WebRtcLocalRenderState.GONE, val reactions: List = emptyList(), + val raisedHands: List = emptyList(), val isInPipMode: Boolean = false, private val showVideoForOutgoing: Boolean = false, val isViewingFocusedParticipant: Boolean = false, @@ -227,6 +229,7 @@ data class CallParticipantsState( localRenderState = localRenderState, showVideoForOutgoing = newShowVideoForOutgoing, recipient = webRtcViewModel.recipient, + raisedHands = webRtcViewModel.raisedHands, remoteDevicesCount = webRtcViewModel.remoteDevicesCount, ringGroup = webRtcViewModel.ringGroup, isInOutgoingRingingMode = isInOutgoingRingingMode, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallReactionScrubber.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallReactionScrubber.kt index d0d26115f0..fb2809a8a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallReactionScrubber.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallReactionScrubber.kt @@ -39,7 +39,7 @@ class CallReactionScrubber @JvmOverloads constructor( customEmojiIndex = emojiViews.size - 1 } - fun initialize(fragmentManager: FragmentManager, callback: ReactWithAnyEmojiBottomSheetDialogFragment.Callback, listener: (String) -> Unit) { + fun initialize(fragmentManager: FragmentManager, listener: (String) -> Unit) { val emojis = SignalStore.emojiValues().reactions for (i in emojiViews.indices) { val view = emojiViews[i] diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java index a55948be76..44540641de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java @@ -24,6 +24,7 @@ import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.widget.Toolbar; +import androidx.compose.ui.platform.ComposeView; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintSet; import androidx.constraintlayout.widget.Guideline; @@ -124,6 +125,8 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { private RecyclerView groupReactionsFeed; private MultiReactionBurstLayout reactionViews; private Guideline aboveControlsGuideline; + private ComposeView raiseHandSnackbar; + private WebRtcCallParticipantsPagerAdapter pagerAdapter; @@ -202,6 +205,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { groupReactionsFeed = findViewById(R.id.call_screen_reactions_feed); reactionViews = findViewById(R.id.call_screen_reactions_container); aboveControlsGuideline = findViewById(R.id.call_screen_above_controls_guideline); + raiseHandSnackbar = findViewById(R.id.call_screen_raise_hand_view); View decline = findViewById(R.id.call_screen_decline_call); View answerLabel = findViewById(R.id.call_screen_answer_call_label); @@ -728,6 +732,10 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { visibleViewSet.add(groupReactionsFeed); } + if (webRtcControls.displayRaiseHand()) { + visibleViewSet.add(raiseHandSnackbar); + } + boolean forceUpdate = webRtcControls.adjustForFold() && !controls.adjustForFold(); controls = webRtcControls; @@ -834,6 +842,12 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { ConstraintSet.TOP, ViewUtil.dpToPx(layoutPositions.reactionBottomMargin)); + constraintSet.connect(R.id.call_screen_raise_hand_view, + ConstraintSet.BOTTOM, + layoutPositions.reactionBottomViewId, + ConstraintSet.TOP, + ViewUtil.dpToPx(layoutPositions.reactionBottomMargin)); + constraintSet.applyTo(this); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java index 2a7b867df1..38a48c4b84 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java @@ -216,6 +216,10 @@ public final class WebRtcControls { return !isInPipMode; } + public boolean displayRaiseHand() { + return FeatureFlags.groupCallRaiseHand() && !isInPipMode; + } + public @NonNull WebRtcAudioOutput getAudioOutput() { switch (activeDevice) { case SPEAKER_PHONE: diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/CallInfoView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/CallInfoView.kt index 64f4515746..fe0af4ff86 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/CallInfoView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/CallInfoView.kt @@ -23,6 +23,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -48,7 +49,9 @@ import org.signal.core.ui.theme.SignalTheme import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.AvatarImageView import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.events.CallParticipant +import org.thoughtcrime.securesms.events.GroupCallRaiseHandEvent import org.thoughtcrime.securesms.events.WebRtcViewModel import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry import org.thoughtcrime.securesms.recipients.Recipient @@ -72,7 +75,8 @@ object CallInfoView { remoteParticipants = state.allRemoteParticipants.sortedBy { it.callParticipantId.recipientId }, localParticipant = state.localParticipant, groupMembers = state.groupMembers.filterNot { it.member.isSelf }, - callRecipient = state.recipient + callRecipient = state.recipient, + raisedHands = state.raisedHands ) } .subscribeAsState(ParticipantsState()) @@ -94,8 +98,9 @@ object CallInfoView { private fun CallInfoPreview() { SignalTheme(isDarkMode = true) { Surface { + val remoteParticipants = listOf(CallParticipant(recipient = Recipient.UNKNOWN)) CallInfo( - participantsState = ParticipantsState(remoteParticipants = listOf(CallParticipant(recipient = Recipient.UNKNOWN))), + participantsState = ParticipantsState(remoteParticipants = remoteParticipants, raisedHands = remoteParticipants.map { GroupCallRaiseHandEvent(it.recipient, System.currentTimeMillis()) }), controlAndInfoState = ControlAndInfoState() ) } @@ -127,6 +132,31 @@ private fun CallInfo( ) } + if (participantsState.raisedHands.isNotEmpty()) { + item { + Box( + modifier = Modifier + .padding(horizontal = 24.dp) + .defaultMinSize(minHeight = 52.dp) + .fillMaxWidth(), + contentAlignment = Alignment.CenterStart + ) { + Text( + text = pluralStringResource(id = R.plurals.CallParticipantsListDialog__raised_hands, count = participantsState.raisedHands.size, participantsState.raisedHands.size), + style = MaterialTheme.typography.titleSmall + ) + } + } + + items( + items = participantsState.raisedHands, + key = { it.sender.id }, + contentType = { null } + ) { + HandRaisedRow(recipient = it.sender) + } + } + item { Box( modifier = Modifier @@ -173,6 +203,8 @@ private fun CallInfo( showIcons = false, isVideoEnabled = false, isMicrophoneEnabled = false, + showHandRaised = false, + canLowerHand = false, isSelfAdmin = false, onBlockClicked = {} ) @@ -214,6 +246,16 @@ private fun CallParticipantRowPreview() { } } +@Preview +@Composable +private fun HandRaisedRowPreview() { + SignalTheme(isDarkMode = true) { + Surface { + HandRaisedRow(Recipient.UNKNOWN, canLowerHand = true) + } + } +} + @Composable private fun CallParticipantRow( callParticipant: CallParticipant, @@ -226,11 +268,28 @@ private fun CallParticipantRow( showIcons = true, isVideoEnabled = callParticipant.isVideoEnabled, isMicrophoneEnabled = callParticipant.isMicrophoneEnabled, + showHandRaised = false, + canLowerHand = false, isSelfAdmin = isSelfAdmin, onBlockClicked = { onBlockClicked(callParticipant) } ) } +@Composable +private fun HandRaisedRow(recipient: Recipient, canLowerHand: Boolean = recipient.isSelf) { + CallParticipantRow( + initialRecipient = recipient, + name = recipient.getShortDisplayName(LocalContext.current), + showIcons = true, + isVideoEnabled = true, + isMicrophoneEnabled = true, + showHandRaised = true, + canLowerHand = canLowerHand, + isSelfAdmin = false, + onBlockClicked = {} + ) +} + @Composable private fun CallParticipantRow( initialRecipient: Recipient, @@ -238,6 +297,8 @@ private fun CallParticipantRow( showIcons: Boolean, isVideoEnabled: Boolean, isMicrophoneEnabled: Boolean, + showHandRaised: Boolean, + canLowerHand: Boolean, isSelfAdmin: Boolean, onBlockClicked: () -> Unit ) { @@ -275,6 +336,25 @@ private fun CallParticipantRow( .align(Alignment.CenterVertically) ) + if (showIcons && showHandRaised && canLowerHand) { + TextButton(onClick = { + if (recipient.isSelf) { + ApplicationDependencies.getSignalCallManager().raiseHand(false) + } + }) { + Text(text = stringResource(id = R.string.CallOverflowPopupWindow__lower_hand)) + } + Spacer(modifier = Modifier.width(16.dp)) + } + + if (showIcons && showHandRaised) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.symbol_raise_hand_24), + contentDescription = null, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + if (showIcons && !isVideoEnabled) { Icon( imageVector = ImageVector.vectorResource(id = R.drawable.symbol_video_slash_24), @@ -322,6 +402,8 @@ private fun GroupMemberRow( showIcons = false, isVideoEnabled = false, isMicrophoneEnabled = false, + showHandRaised = false, + canLowerHand = false, isSelfAdmin = isSelfAdmin ) {} } @@ -334,7 +416,8 @@ private data class ParticipantsState( val remoteParticipants: List = emptyList(), val localParticipant: CallParticipant? = null, val groupMembers: List = emptyList(), - val callRecipient: Recipient = Recipient.UNKNOWN + val callRecipient: Recipient = Recipient.UNKNOWN, + val raisedHands: List = emptyList() ) { val participantsForList: List = if (includeSelf && localParticipant != null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/ControlsAndInfoController.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/ControlsAndInfoController.kt index 4c06dec924..895179b2ff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/ControlsAndInfoController.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/ControlsAndInfoController.kt @@ -69,6 +69,7 @@ class ControlsAndInfoController( private val frame: FrameLayout private val behavior: BottomSheetBehavior private val callInfoComposeView: ComposeView + private val raiseHandComposeView: ComposeView private val callControls: ConstraintLayout private val bottomSheetVisibilityListeners = mutableSetOf() private val scheduleHideControlsRunnable: Runnable = Runnable { onScheduledHide() } @@ -86,6 +87,7 @@ class ControlsAndInfoController( behavior = BottomSheetBehavior.from(frame) callInfoComposeView = webRtcCallView.findViewById(R.id.call_info_compose) callControls = webRtcCallView.findViewById(R.id.call_controls_constraint_layout) + raiseHandComposeView = webRtcCallView.findViewById(R.id.call_screen_raise_hand_view) callInfoComposeView.apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) @@ -98,6 +100,13 @@ class ControlsAndInfoController( callInfoComposeView.alpha = 0f callInfoComposeView.translationY = infoTranslationDistance + raiseHandComposeView.apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + RaiseHandSnackbar.View(viewModel, showCallInfoListener = ::showCallInfo) + } + } + frame.background = MaterialShapeDrawable( ShapeAppearanceModel.builder() .setTopLeftCorner(CornerFamily.ROUNDED, 18.dp.toFloat()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/RaiseHandSnackbar.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/RaiseHandSnackbar.kt new file mode 100644 index 0000000000..f69edba841 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/RaiseHandSnackbar.kt @@ -0,0 +1,217 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.webrtc.controls + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rxjava3.subscribeAsState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.reactivex.rxjava3.core.BackpressureStrategy +import kotlinx.coroutines.delay +import org.signal.core.ui.theme.SignalTheme +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.events.GroupCallRaiseHandEvent +import org.thoughtcrime.securesms.recipients.Recipient +import java.util.concurrent.TimeUnit +import kotlin.time.Duration + +/** + * This is a UI element to display the status of one or more people with raised hands in a group call. + * It supports both an expanded and collapsed mode. + */ +object RaiseHandSnackbar { + const val TAG = "RaiseHandSnackbar" + val COLLAPSE_DELAY_MS = TimeUnit.SECONDS.toMillis(4L) + + @Composable + fun View(webRtcCallViewModel: WebRtcCallViewModel, showCallInfoListener: () -> Unit, modifier: Modifier = Modifier) { + var isExpanded by remember { mutableStateOf(ExpansionState(false, false)) } + + val webRtcState by webRtcCallViewModel.callParticipantsState + .toFlowable(BackpressureStrategy.LATEST) + .map { state -> + val raisedHands = state.raisedHands.sortedByDescending { it.timestamp } + val shouldExpand = RaiseHandState.shouldExpand(raisedHands) + if (!isExpanded.forced) { + isExpanded = ExpansionState(shouldExpand, false) + } + raisedHands + }.subscribeAsState(initial = emptyList()) + + val state by remember { + derivedStateOf { + RaiseHandState(raisedHands = webRtcState, expansionState = isExpanded) + } + } + + LaunchedEffect(isExpanded) { + delay(COLLAPSE_DELAY_MS) + isExpanded = ExpansionState(false, false) + } + + RaiseHand(state, modifier, { isExpanded = ExpansionState(true, true) }, showCallInfoListener) + } +} + +@Preview +@Composable +private fun RaiseHandSnackbarPreview() { + RaiseHand( + state = RaiseHandState(listOf(GroupCallRaiseHandEvent(Recipient.UNKNOWN, System.currentTimeMillis()))) + ) +} + +@Composable +private fun RaiseHand( + state: RaiseHandState, + modifier: Modifier = Modifier, + setExpanded: (Boolean) -> Unit = {}, + showCallInfoListener: () -> Unit = {} +) { + AnimatedVisibility(visible = state.raisedHands.isNotEmpty()) { + SignalTheme( + isDarkMode = true + ) { + Surface( + modifier = modifier + .padding(horizontal = 16.dp) + .clip(shape = RoundedCornerShape(16.dp, 16.dp, 16.dp, 16.dp)) + .background(MaterialTheme.colorScheme.surface) + ) { + val boxModifier = modifier + .height(48.dp) + .animateContentSize() + .padding(horizontal = 16.dp) + .clickable( + !state.expansionState.isExpanded, + stringResource(id = R.string.CallOverflowPopupWindow__expand_snackbar_accessibility_label), + Role.Button + ) { setExpanded(true) } + + Box( + contentAlignment = Alignment.CenterStart, + modifier = if (state.expansionState.isExpanded) { + boxModifier.fillMaxWidth() + } else { + boxModifier.wrapContentWidth() + } + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.symbol_raise_hand_24), + contentDescription = null, + modifier = Modifier.align(Alignment.CenterVertically) + ) + + Text( + text = getSnackbarText(state), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(start = 16.dp) + ) + if (state.expansionState.isExpanded && state.raisedHands.isNotEmpty()) { + Spacer(modifier = Modifier.weight(1f)) + + if (state.raisedHands.first().sender.isSelf) { + TextButton(onClick = { + ApplicationDependencies.getSignalCallManager().raiseHand(false) + }) { + Text(text = stringResource(id = R.string.CallOverflowPopupWindow__lower_hand)) + } + } else { + TextButton(onClick = showCallInfoListener) { + Text(text = stringResource(id = R.string.CallOverflowPopupWindow__view)) + } + } + } + } + } + } + } + } +} + +@Composable +private fun getSnackbarText(state: RaiseHandState): String { + if (state.isEmpty()) { + return "" + } + return if (!state.expansionState.isExpanded) { + pluralStringResource(id = R.plurals.CallRaiseHandSnackbar_raised_hands, count = state.raisedHands.size, getShortDisplayName(state.raisedHands), state.raisedHands.size - 1) + } else { + if (state.raisedHands.size == 1 && state.raisedHands.first().sender.isSelf) { + stringResource(id = R.string.CallOverflowPopupWindow__you_raised_your_hand) + } else { + pluralStringResource(id = R.plurals.CallOverflowPopupWindow__raised_a_hand, count = state.raisedHands.size, state.raisedHands.first().sender.getShortDisplayName(LocalContext.current), state.raisedHands.size - 1) + } + } +} + +@Composable +private fun getShortDisplayName(raisedHands: List): String { + val recipient = raisedHands.first().sender + return if (recipient.isSelf) { + stringResource(id = R.string.CallParticipant__you) + } else { + recipient.getShortDisplayName(LocalContext.current) + } +} + +private data class RaiseHandState( + val raisedHands: List = emptyList(), + val expansionState: ExpansionState = ExpansionState(false, false) +) { + + fun isEmpty(): Boolean { + return raisedHands.isEmpty() + } + + companion object { + @JvmStatic + fun shouldExpand(raisedHands: List): Boolean { + val now = System.currentTimeMillis() + return raisedHands.any { it.getCollapseTimestamp() > now } + } + } +} + +private data class ExpansionState( + val isExpanded: Boolean, + val forced: Boolean +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.kt b/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.kt index 5583b3c978..fc0745c302 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.kt @@ -16,6 +16,7 @@ data class CallParticipant constructor( val isForwardingVideo: Boolean = true, val isVideoEnabled: Boolean = false, val isMicrophoneEnabled: Boolean = false, + val isHandRaised: Boolean = false, val lastSpoke: Long = 0, val audioLevel: AudioLevel? = null, val isMediaKeysReceived: Boolean = true, @@ -109,7 +110,8 @@ data class CallParticipant constructor( fun createLocal( cameraState: CameraState, renderer: BroadcastVideoSink, - microphoneEnabled: Boolean + microphoneEnabled: Boolean, + isHandRaised: Boolean ): CallParticipant { return CallParticipant( callParticipantId = CallParticipantId(Recipient.self()), @@ -117,7 +119,8 @@ data class CallParticipant constructor( videoSink = renderer, cameraState = cameraState, isVideoEnabled = cameraState.isEnabled && cameraState.cameraCount > 0, - isMicrophoneEnabled = microphoneEnabled + isMicrophoneEnabled = microphoneEnabled, + isHandRaised = isHandRaised ) } @@ -130,6 +133,7 @@ data class CallParticipant constructor( isForwardingVideo: Boolean, audioEnabled: Boolean, videoEnabled: Boolean, + isHandRaised: Boolean, lastSpoke: Long, mediaKeysReceived: Boolean, addedToCallTime: Long, @@ -144,6 +148,7 @@ data class CallParticipant constructor( isForwardingVideo = isForwardingVideo, isVideoEnabled = videoEnabled, isMicrophoneEnabled = audioEnabled, + isHandRaised = isHandRaised, lastSpoke = lastSpoke, isMediaKeysReceived = mediaKeysReceived, addedToCallTime = addedToCallTime, diff --git a/app/src/main/java/org/thoughtcrime/securesms/events/GroupCallRaiseHandEvent.kt b/app/src/main/java/org/thoughtcrime/securesms/events/GroupCallRaiseHandEvent.kt new file mode 100644 index 0000000000..3356117f08 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/events/GroupCallRaiseHandEvent.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.events + +import org.thoughtcrime.securesms.recipients.Recipient +import java.util.concurrent.TimeUnit + +data class GroupCallRaiseHandEvent(val sender: Recipient, val timestamp: Long) { + fun getCollapseTimestamp(): Long { + return timestamp + TimeUnit.SECONDS.toMillis(LIFESPAN_SECONDS) + } + + companion object { + const val LIFESPAN_SECONDS = 4L + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.kt index e7d2e71591..2bee6b75da 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.kt @@ -92,6 +92,7 @@ class WebRtcViewModel(state: WebRtcServiceState) { val isRemoteVideoOffer: Boolean = state.getCallSetupState(state.callInfoState.activePeer?.callId).isRemoteVideoOffer val callConnectedTime: Long = state.callInfoState.callConnectedTime val remoteParticipants: List = state.callInfoState.remoteCallParticipants + val raisedHands: List = state.callInfoState.raisedHands val identityChangedParticipants: Set = state.callInfoState.identityChangedRecipients val remoteDevicesCount: OptionalLong = state.callInfoState.remoteDevicesCount val participantLimit: Long? = state.callInfoState.participantLimit @@ -109,7 +110,8 @@ class WebRtcViewModel(state: WebRtcServiceState) { val localParticipant: CallParticipant = createLocal( state.localDeviceState.cameraState, (if (state.videoState.localSink != null) state.videoState.localSink else BroadcastVideoSink())!!, - state.localDeviceState.isMicrophoneEnabled + state.localDeviceState.isMicrophoneEnabled, + state.callInfoState.raisedHands.map { it.sender }.contains(Recipient.self()) ) val isCellularConnection: Boolean = when (state.localDeviceState.networkConnectionType) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/BeginCallActionProcessorDelegate.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/BeginCallActionProcessorDelegate.java index 3c7903e31e..7615277d08 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/BeginCallActionProcessorDelegate.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/BeginCallActionProcessorDelegate.java @@ -50,6 +50,7 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor { true, true, false, + currentState.getCallInfoState().getRaisedHands().contains(remotePeer.getRecipient()), 0, true, 0, @@ -108,6 +109,7 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor { true, true, false, + currentState.getCallInfoState().getRaisedHands().contains(remotePeer.getRecipient()), 0, true, 0, diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java index 08f92877b5..f161f47f62 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java @@ -116,6 +116,7 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor { device.getForwardingVideo() == null || device.getForwardingVideo(), Boolean.FALSE.equals(device.getAudioMuted()), Boolean.FALSE.equals(device.getVideoMuted()), + currentState.getCallInfoState().getRaisedHands().contains(recipient), device.getSpeakerTime(), device.getMediaKeysReceived(), device.getAddedTime(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java index 579e8e3cb2..333dc470b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java @@ -14,6 +14,7 @@ import org.signal.ringrtc.GroupCall; import org.signal.ringrtc.PeekInfo; import org.thoughtcrime.securesms.events.CallParticipant; import org.thoughtcrime.securesms.events.CallParticipantId; +import org.thoughtcrime.securesms.events.GroupCallRaiseHandEvent; import org.thoughtcrime.securesms.events.GroupCallReactionEvent; import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.keyvalue.SignalStore; @@ -26,10 +27,15 @@ import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; + +import ezvcard.util.StringUtils; /** * Process actions for when the call has at least once been connected and joined. @@ -202,6 +208,21 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor { return terminateGroupCall(currentState); } + @Override + protected @NonNull WebRtcServiceState handleSelfRaiseHand(@NonNull WebRtcServiceState currentState, boolean raised) { + Log.i(tag, "handleSelfRaiseHand():"); + try { + final CallInfoState callInfoState = currentState.getCallInfoState(); + + callInfoState.requireGroupCall().raiseHand(raised); + + return currentState; + } catch (CallException e) { + Log.w(TAG, "Unable to " + (raised ? "raise" : "lower") + " hand in group call", e); + } + return currentState; + } + @Override protected @NonNull WebRtcEphemeralState handleSendGroupReact(@NonNull WebRtcServiceState currentState, @NonNull WebRtcEphemeralState ephemeralState, @NonNull String reaction) { try { @@ -242,4 +263,37 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor { return new GroupCallReactionEvent(participant.getRecipient(), reaction.value, System.currentTimeMillis()); } + + @Override + protected @NonNull WebRtcServiceState handleGroupCallRaisedHand(@NonNull WebRtcServiceState currentState, List raisedHands) { + Log.i(tag, "handleGroupCallRaisedHand():"); + List existingHands = currentState.getCallInfoState().getRaisedHands(); + + List participants = currentState.getCallInfoState().getRemoteCallParticipants(); + List currentRaisedHands = raisedHands + .stream().map(demuxId -> { + if (Objects.equals(demuxId, currentState.getCallInfoState().requireGroupCall().getLocalDeviceState().getDemuxId())) { + return Recipient.self(); + } + + CallParticipant participant = participants.stream().filter(it -> it.getCallParticipantId().getDemuxId() == demuxId).findFirst().orElse(null); + if (participant == null) { + Log.v(TAG, "Could not find CallParticipantId in list of call participants based on demuxId for raise hand."); + return null; + } + return participant.getRecipient(); + }) + .filter(Objects::nonNull) + .map(recipient -> { + final Optional matchingEvent = existingHands.stream().filter(existingEvent -> existingEvent.getSender().equals(recipient)).findFirst(); + if (matchingEvent.isPresent()) { + return matchingEvent.get(); + } else { + return new GroupCallRaiseHandEvent(recipient, System.currentTimeMillis()); + } + }) + .collect(Collectors.toList()); + + return currentState.builder().changeCallInfoState().setRaisedHand(currentRaisedHands).build(); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupPreJoinActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupPreJoinActionProcessor.java index 3e60ebebc9..52b1380a28 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupPreJoinActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupPreJoinActionProcessor.java @@ -137,6 +137,7 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor { true, true, true, + currentState.getCallInfoState().getRaisedHands().contains(recipient), 0, false, 0, diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingGroupCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingGroupCallActionProcessor.java index dc6f2b62a5..683bdaebe0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingGroupCallActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingGroupCallActionProcessor.java @@ -167,6 +167,7 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro true, true, false, + currentState.getCallInfoState().getRaisedHands().contains(remotePeerGroup.getRecipient()), 0, true, 0, diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java index f339bcdc21..a178af0ef7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java @@ -295,6 +295,10 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. process((s, p) -> p.handleScreenOffChange(s)); } + public void raiseHand(boolean raised) { + process((s, p) -> p.handleSelfRaiseHand(s, raised)); + } + public void react(@NonNull String reaction) { processStateless(s -> serviceState.getActionProcessor().handleSendGroupReact(serviceState, s, reaction)); } @@ -909,7 +913,9 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. @Override public void onRaisedHands(@NonNull GroupCall groupCall, List raisedHands) { - // TODO: Implement handling of raise hand. + if (FeatureFlags.groupCallRaiseHand()) { + process((s, p) -> p.handleGroupCallRaisedHand(s, raisedHands)); + } } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java index 9d30723d53..ff628e20aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java @@ -546,6 +546,12 @@ public abstract class WebRtcActionProcessor { return currentState; } + + protected @NonNull WebRtcServiceState handleSelfRaiseHand(@NonNull WebRtcServiceState currentState, boolean raised) { + Log.i(tag, "raiseHand not processed"); + return currentState; + } + protected @NonNull WebRtcEphemeralState handleSendGroupReact(@NonNull WebRtcServiceState currentState, @NonNull WebRtcEphemeralState ephemeralState, @NonNull String reaction) { Log.i(tag, "react not processed"); return ephemeralState; @@ -739,6 +745,11 @@ public abstract class WebRtcActionProcessor { return ephemeralState; } + protected @NonNull WebRtcServiceState handleGroupCallRaisedHand(@NonNull WebRtcServiceState currentState, List raisedHands) { + Log.i(tag, "handleGroupCallRaisedHand not processed"); + return currentState; + } + protected @NonNull WebRtcServiceState handleGroupRequestMembershipProof(@NonNull WebRtcServiceState currentState, int groupCallHashCode) { Log.i(tag, "handleGroupRequestMembershipProof not processed"); return currentState; diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallInfoState.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallInfoState.kt index 511aa45993..0bdd58482b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallInfoState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/CallInfoState.kt @@ -5,6 +5,7 @@ import org.signal.ringrtc.CallId import org.signal.ringrtc.GroupCall import org.thoughtcrime.securesms.events.CallParticipant import org.thoughtcrime.securesms.events.CallParticipantId +import org.thoughtcrime.securesms.events.GroupCallRaiseHandEvent import org.thoughtcrime.securesms.events.WebRtcViewModel import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId @@ -30,7 +31,8 @@ data class CallInfoState( var remoteDevicesCount: OptionalLong = OptionalLong.empty(), var participantLimit: Long? = null, var pendingParticipants: PendingParticipantCollection = PendingParticipantCollection(), - var callLinkDisconnectReason: CallLinkDisconnectReason? = null + var callLinkDisconnectReason: CallLinkDisconnectReason? = null, + var raisedHands: List = emptyList() ) { val remoteCallParticipants: List diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java index 8a32ec5378..cc4f8ea375 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java @@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink; import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper; import org.thoughtcrime.securesms.events.CallParticipant; import org.thoughtcrime.securesms.events.CallParticipantId; +import org.thoughtcrime.securesms.events.GroupCallRaiseHandEvent; import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -364,5 +365,10 @@ public class WebRtcServiceStateBuilder { toBuild.setCallLinkDisconnectReason(callLinkDisconnectReason); return this; } + + public @NonNull CallInfoStateBuilder setRaisedHand(@NonNull List raisedHands) { + toBuild.setRaisedHands(raisedHands); + return this; + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index cbb410b679..a087836f8d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -118,6 +118,7 @@ public final class FeatureFlags { public static final String SEPA_ENABLED_REGIONS = "global.donations.sepaEnabledRegions"; private static final String CALLING_REACTIONS = "android.calling.reactions"; private static final String NOTIFICATION_THUMBNAIL_BLOCKLIST = "android.notificationThumbnailProductBlocklist"; + private static final String CALLING_RAISE_HAND = "android.calling.raiseHand"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable @@ -187,7 +188,8 @@ public final class FeatureFlags { IDEAL_ENABLED_REGIONS, SEPA_ENABLED_REGIONS, CALLING_REACTIONS, - NOTIFICATION_THUMBNAIL_BLOCKLIST + NOTIFICATION_THUMBNAIL_BLOCKLIST, + CALLING_RAISE_HAND ); @VisibleForTesting @@ -259,7 +261,8 @@ public final class FeatureFlags { CRASH_PROMPT_CONFIG, BLOCK_SSE, CALLING_REACTIONS, - NOTIFICATION_THUMBNAIL_BLOCKLIST + NOTIFICATION_THUMBNAIL_BLOCKLIST, + CALLING_RAISE_HAND ); /** @@ -672,6 +675,13 @@ public final class FeatureFlags { return getBoolean(CALLING_REACTIONS, false); } + /** + * Whether or not group call raise hand is enabled. + */ + public static boolean groupCallRaiseHand() { + return getBoolean(CALLING_RAISE_HAND, false); + } + /** List of device products that are blocked from showing notification thumbnails. */ public static String notificationThumbnailProductBlocklist() { return getString(NOTIFICATION_THUMBNAIL_BLOCKLIST, ""); diff --git a/app/src/main/res/drawable/symbol_raise_hand_24.xml b/app/src/main/res/drawable/symbol_raise_hand_24.xml new file mode 100644 index 0000000000..fdef3b6a55 --- /dev/null +++ b/app/src/main/res/drawable/symbol_raise_hand_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/call_overflow_holder.xml b/app/src/main/res/layout/call_overflow_holder.xml index 90d618dc57..3c9c043c79 100644 --- a/app/src/main/res/layout/call_overflow_holder.xml +++ b/app/src/main/res/layout/call_overflow_holder.xml @@ -3,10 +3,50 @@ ~ SPDX-License-Identifier: AGPL-3.0-only --> - \ No newline at end of file + android:orientation="vertical"> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/webrtc_call_view.xml b/app/src/main/res/layout/webrtc_call_view.xml index 960fa58f91..3001913e0e 100644 --- a/app/src/main/res/layout/webrtc_call_view.xml +++ b/app/src/main/res/layout/webrtc_call_view.xml @@ -21,6 +21,7 @@ android:orientation="horizontal" tools:layout_constraintGuide_end="200dp" /> + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index d2ac93adc0..1ab4058c68 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -79,9 +79,15 @@ 25dp 0dp 320dp + 4dp 48dp + 320dp + 120dp + + 16dp + 38dp 28dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9d89df33e2..a5f431d940 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1849,6 +1849,34 @@ An icon representing a device\'s earpiece. + + Raise hand + + Raise hand + + Lower your hand? + + Lower hand + + You raised your hand + + View + + + + %1$s has raised a hand + %1$s + %2$d have raised a hand + + + + + %1$s + %1$s +%2$d + + + + Expand raised hand view + In this call (%1$d) @@ -1862,6 +1890,10 @@ Signal will Notify (%1$d) Signal will Notify (%1$d) + + Raised hand (%1$d) + Raised hands (%1$d) + %1$s is blocked diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantListUpdateTest.java b/app/src/test/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantListUpdateTest.java index 954accb427..eadb9a1418 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantListUpdateTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantListUpdateTest.java @@ -178,7 +178,7 @@ public class CallParticipantListUpdateTest { private static CallParticipant createParticipant(long recipientId, long deMuxId, @NonNull CallParticipant.DeviceOrdinal deviceOrdinal) { Recipient recipient = new Recipient(RecipientId.from(recipientId), mock(RecipientDetails.class), true); - return CallParticipant.createRemote(new CallParticipantId(deMuxId, recipient.getId()), recipient, null, new BroadcastVideoSink(), false, false, false, -1, false, 0, false, deviceOrdinal); + return CallParticipant.createRemote(new CallParticipantId(deMuxId, recipient.getId()), recipient, null, new BroadcastVideoSink(), false, false, false, false, -1, false, 0, false, deviceOrdinal); } } \ No newline at end of file diff --git a/app/src/test/java/org/thoughtcrime/securesms/service/webrtc/collections/ParticipantCollectionTest.java b/app/src/test/java/org/thoughtcrime/securesms/service/webrtc/collections/ParticipantCollectionTest.java index cfc80380c7..d7c8fce289 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/service/webrtc/collections/ParticipantCollectionTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/service/webrtc/collections/ParticipantCollectionTest.java @@ -243,6 +243,7 @@ public class ParticipantCollectionTest { false, false, false, + false, lastSpoke, false, added,