mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-03-04 00:08:35 +00:00
Add additional call links moderation ui.
This commit is contained in:
@@ -62,6 +62,7 @@ import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallStateUpdatePopupWindow;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallToastPopupWindow;
|
||||
import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil;
|
||||
import org.thoughtcrime.securesms.components.webrtc.InCallStatus;
|
||||
import org.thoughtcrime.securesms.components.webrtc.PendingParticipantsBottomSheet;
|
||||
import org.thoughtcrime.securesms.components.webrtc.PendingParticipantsView;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioDevice;
|
||||
@@ -98,6 +99,7 @@ import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
@@ -114,7 +116,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
/**
|
||||
* ANSWER the call via voice-only.
|
||||
*/
|
||||
public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
|
||||
public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
|
||||
|
||||
/**
|
||||
* ANSWER the call via video.
|
||||
@@ -125,6 +127,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
|
||||
public static final String EXTRA_ENABLE_VIDEO_IF_AVAILABLE = WebRtcCallActivity.class.getCanonicalName() + ".ENABLE_VIDEO_IF_AVAILABLE";
|
||||
public static final String EXTRA_STARTED_FROM_FULLSCREEN = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_FULLSCREEN";
|
||||
public static final String EXTRA_STARTED_FROM_CALL_LINK = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_CALL_LINK";
|
||||
|
||||
private CallParticipantsListUpdatePopupWindow participantUpdateWindow;
|
||||
private CallStateUpdatePopupWindow callStateUpdatePopupWindow;
|
||||
@@ -198,6 +201,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
windowInfoTrackerCallbackAdapter.addWindowLayoutInfoListener(this, SignalExecutors.BOUNDED, windowLayoutInfoConsumer);
|
||||
|
||||
requestNewSizesThrottle = new ThrottledDebouncer(TimeUnit.SECONDS.toMillis(1));
|
||||
|
||||
initializePendingParticipantFragmentListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -344,6 +349,54 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
}
|
||||
|
||||
private void initializePendingParticipantFragmentListener() {
|
||||
if (!FeatureFlags.adHocCalling()) {
|
||||
return;
|
||||
}
|
||||
|
||||
getSupportFragmentManager().setFragmentResultListener(
|
||||
PendingParticipantsBottomSheet.REQUEST_KEY,
|
||||
this,
|
||||
(requestKey, result) -> {
|
||||
PendingParticipantsBottomSheet.Action action = PendingParticipantsBottomSheet.getAction(result);
|
||||
List<RecipientId> recipientIds = viewModel.getPendingParticipantsSnapshot()
|
||||
.getUnresolvedPendingParticipants()
|
||||
.stream()
|
||||
.map(r -> r.getRecipient().getId())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
switch (action) {
|
||||
case NONE:
|
||||
break;
|
||||
case APPROVE_ALL:
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getResources().getQuantityString(R.plurals.WebRtcCallActivity__approve_d_requests, recipientIds.size(), recipientIds.size()))
|
||||
.setMessage(getResources().getQuantityString(R.plurals.WebRtcCallActivity__d_people_will_be_added_to_the_call, recipientIds.size(), recipientIds.size()))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.WebRtcCallActivity__approve_all, (dialog, which) -> {
|
||||
for (RecipientId id : recipientIds) {
|
||||
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(id);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
break;
|
||||
case DENY_ALL:
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getResources().getQuantityString(R.plurals.WebRtcCallActivity__deny_d_requests, recipientIds.size(), recipientIds.size()))
|
||||
.setMessage(getResources().getQuantityString(R.plurals.WebRtcCallActivity__d_people_will_be_added_to_the_call, recipientIds.size(), recipientIds.size()))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.WebRtcCallActivity__deny_all, (dialog, which) -> {
|
||||
for (RecipientId id : recipientIds) {
|
||||
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(id);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private void initializeScreenshotSecurity() {
|
||||
if (TextSecurePreferences.isScreenSecurityEnabled(this)) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
@@ -373,12 +426,14 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
|
||||
viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls);
|
||||
viewModel.getEvents().observe(this, this::handleViewModelEvent);
|
||||
viewModel.getCallTime().observe(this, this::handleCallTime);
|
||||
|
||||
lifecycleDisposable.add(viewModel.getInCallstatus().subscribe(this::handleInCallStatus));
|
||||
|
||||
boolean isStartedFromCallLink = getIntent().getBooleanExtra(WebRtcCallActivity.EXTRA_STARTED_FROM_CALL_LINK, false);
|
||||
LiveDataUtil.combineLatest(viewModel.getCallParticipantsState(),
|
||||
viewModel.getOrientationAndLandscapeEnabled(),
|
||||
viewModel.getEphemeralState(),
|
||||
(s, o, e) -> new CallParticipantsViewState(s, e, o.first == PORTRAIT_BOTTOM_EDGE, o.second))
|
||||
(s, o, e) -> new CallParticipantsViewState(s, e, o.first == PORTRAIT_BOTTOM_EDGE, o.second, isStartedFromCallLink))
|
||||
.observe(this, p -> callScreen.updateCallParticipants(p));
|
||||
viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate);
|
||||
viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent);
|
||||
@@ -474,14 +529,27 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCallTime(long callTime) {
|
||||
EllapsedTimeFormatter ellapsedTimeFormatter = EllapsedTimeFormatter.fromDurationMillis(callTime);
|
||||
private void handleInCallStatus(@NonNull InCallStatus inCallStatus) {
|
||||
if (inCallStatus instanceof InCallStatus.ElapsedTime) {
|
||||
|
||||
if (ellapsedTimeFormatter == null) {
|
||||
return;
|
||||
EllapsedTimeFormatter ellapsedTimeFormatter = EllapsedTimeFormatter.fromDurationMillis(((InCallStatus.ElapsedTime) inCallStatus).getElapsedTime());
|
||||
|
||||
if (ellapsedTimeFormatter == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
callScreen.setStatus(getString(R.string.WebRtcCallActivity__signal_s, ellapsedTimeFormatter.toString()));
|
||||
} else if (inCallStatus instanceof InCallStatus.PendingUsers) {
|
||||
int waiting = ((InCallStatus.PendingUsers) inCallStatus).getPendingUserCount();
|
||||
|
||||
callScreen.setStatus(getResources().getQuantityString(
|
||||
R.plurals.WebRtcCallActivity__d_people_waiting,
|
||||
waiting,
|
||||
waiting
|
||||
));
|
||||
} else {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
callScreen.setStatus(getString(R.string.WebRtcCallActivity__signal_s, ellapsedTimeFormatter.toString()));
|
||||
}
|
||||
|
||||
private void handleSetAudioHandset() {
|
||||
@@ -702,7 +770,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageResentAfterSafetyNumberChange() { }
|
||||
public void onMessageResentAfterSafetyNumberChange() {}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
@@ -968,12 +1036,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
|
||||
@Override
|
||||
public void onAllowPendingRecipient(@NonNull Recipient pendingRecipient) {
|
||||
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(pendingRecipient);
|
||||
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(pendingRecipient.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRejectPendingRecipient(@NonNull Recipient pendingRecipient) {
|
||||
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(pendingRecipient);
|
||||
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(pendingRecipient.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -28,6 +28,7 @@ import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
@@ -35,7 +36,6 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColorPair
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
@@ -49,10 +49,10 @@ import java.time.Instant
|
||||
@Composable
|
||||
private fun SignalCallRowPreview() {
|
||||
val callLink = remember {
|
||||
val credentials = CallLinkCredentials.generate()
|
||||
val credentials = CallLinkCredentials(byteArrayOf(1, 2, 3, 4), byteArrayOf(5, 6, 7, 8))
|
||||
CallLinkTable.CallLink(
|
||||
recipientId = RecipientId.UNKNOWN,
|
||||
roomId = CallLinkRoomId.fromCallLinkRootKey(CallLinkRootKey(credentials.linkKeyBytes)),
|
||||
roomId = CallLinkRoomId.fromBytes(byteArrayOf(1, 3, 5, 7)),
|
||||
credentials = credentials,
|
||||
state = SignalCallLinkState(
|
||||
name = "Call Name",
|
||||
@@ -76,6 +76,14 @@ fun SignalCallRow(
|
||||
onJoinClicked: (() -> Unit)?,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val callUrl = if (LocalInspectionMode.current) {
|
||||
"https://signal.call.example.com"
|
||||
} else {
|
||||
remember(callLink.credentials) {
|
||||
callLink.credentials?.let { CallLinks.url(it.linkKeyBytes) } ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
@@ -113,7 +121,7 @@ fun SignalCallRow(
|
||||
text = callLink.state.name.ifEmpty { stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__signal_call) }
|
||||
)
|
||||
Text(
|
||||
text = callLink.credentials?.let { CallLinks.url(it.linkKeyBytes) } ?: "",
|
||||
text = callUrl,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
@@ -32,8 +32,10 @@ import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
@@ -45,6 +47,7 @@ import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -66,6 +69,8 @@ import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsViewModel
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.events.CallParticipant
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
@@ -115,8 +120,8 @@ class CallLinkInfoSheet : ComposeBottomSheetDialogFragment() {
|
||||
override fun SheetContent() {
|
||||
val callLinkDetailsState by callLinkDetailsViewModel.state
|
||||
val callParticipantsState by webRtcCallViewModel.callParticipantsState.observeAsState()
|
||||
val participants = if (callParticipantsState?.callState == WebRtcViewModel.State.CALL_CONNECTED) {
|
||||
listOf(Recipient.self()) + (callParticipantsState?.allRemoteParticipants?.map { it.recipient } ?: emptyList())
|
||||
val participants: ImmutableList<CallParticipant> = if (callParticipantsState?.callState == WebRtcViewModel.State.CALL_CONNECTED) {
|
||||
listOf(CallParticipant(recipient = Recipient.self())) + (callParticipantsState?.allRemoteParticipants?.map { it } ?: emptyList())
|
||||
} else {
|
||||
emptyList()
|
||||
}.toImmutableList()
|
||||
@@ -137,11 +142,24 @@ class CallLinkInfoSheet : ComposeBottomSheetDialogFragment() {
|
||||
onShareLinkClicked = this::shareLink,
|
||||
onEditNameClicked = onEditNameClicked,
|
||||
onToggleAdminApprovalClicked = this::onApproveAllMembersChanged,
|
||||
onBlock = {} // TODO [alex] -- Blocking
|
||||
onBlock = this::onBlockParticipant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBlockParticipant(callParticipant: CallParticipant) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setMessage(getString(R.string.CallLinkInfoSheet__remove_s_from_the_call, callParticipant.recipient.getShortDisplayName(requireContext())))
|
||||
.setPositiveButton(R.string.CallLinkInfoSheet__remove) { _, _ ->
|
||||
ApplicationDependencies.getSignalCallManager().removeFromCallLink(callParticipant)
|
||||
}
|
||||
.setNeutralButton(R.string.CallLinkInfoSheet__block_from_call) { _, _ ->
|
||||
ApplicationDependencies.getSignalCallManager().blockFromCallLink(callParticipant)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun onApproveAllMembersChanged(checked: Boolean) {
|
||||
callLinkDetailsViewModel.setApproveAllMembers(checked)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
@@ -210,7 +228,7 @@ private fun SheetPreview() {
|
||||
),
|
||||
state = SignalCallLinkState()
|
||||
),
|
||||
participants = listOf(Recipient.UNKNOWN).toImmutableList(),
|
||||
participants = listOf(CallParticipant(recipient = Recipient.UNKNOWN)).toImmutableList(),
|
||||
onShareLinkClicked = {},
|
||||
onEditNameClicked = {},
|
||||
onToggleAdminApprovalClicked = {},
|
||||
@@ -223,17 +241,24 @@ private fun SheetPreview() {
|
||||
@Composable
|
||||
private fun Sheet(
|
||||
callLink: CallLinkTable.CallLink,
|
||||
participants: ImmutableList<Recipient>,
|
||||
participants: ImmutableList<CallParticipant>,
|
||||
onShareLinkClicked: () -> Unit,
|
||||
onEditNameClicked: () -> Unit,
|
||||
onToggleAdminApprovalClicked: (Boolean) -> Unit,
|
||||
onBlock: (Recipient) -> Unit
|
||||
onBlock: (CallParticipant) -> Unit
|
||||
) {
|
||||
LazyColumn(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
item {
|
||||
BottomSheets.Handle()
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.CallLinkInfoSheet__call_info),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(vertical = 24.dp)
|
||||
)
|
||||
|
||||
SignalCallRow(callLink = callLink, onJoinClicked = null)
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CallLinkDetailsFragment__share_link),
|
||||
@@ -251,9 +276,9 @@ private fun Sheet(
|
||||
)
|
||||
}
|
||||
|
||||
items(participants, { it.id }, { null }) {
|
||||
items(participants, { it.callParticipantId }, { null }) {
|
||||
CallLinkMemberRow(
|
||||
recipient = it,
|
||||
callParticipant = it,
|
||||
isSelfAdmin = callLink.credentials?.adminPassBytes != null,
|
||||
onBlockClicked = onBlock
|
||||
)
|
||||
@@ -282,7 +307,7 @@ private fun CallLinkMemberRowPreview() {
|
||||
SignalTheme(isDarkMode = true) {
|
||||
Surface {
|
||||
CallLinkMemberRow(
|
||||
Recipient.UNKNOWN,
|
||||
CallParticipant(recipient = Recipient.UNKNOWN),
|
||||
isSelfAdmin = true,
|
||||
{}
|
||||
)
|
||||
@@ -292,37 +317,45 @@ private fun CallLinkMemberRowPreview() {
|
||||
|
||||
@Composable
|
||||
private fun CallLinkMemberRow(
|
||||
recipient: Recipient,
|
||||
callParticipant: CallParticipant,
|
||||
isSelfAdmin: Boolean,
|
||||
onBlockClicked: (Recipient) -> Unit
|
||||
onBlockClicked: (CallParticipant) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(Rows.defaultPadding())
|
||||
) {
|
||||
AndroidView(
|
||||
factory = ::AvatarImageView,
|
||||
modifier = Modifier.size(40.dp)
|
||||
) {
|
||||
it.setAvatarUsingProfile(recipient)
|
||||
if (LocalInspectionMode.current) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(color = Color.Red, shape = CircleShape)
|
||||
)
|
||||
} else {
|
||||
AndroidView(
|
||||
factory = ::AvatarImageView,
|
||||
modifier = Modifier.size(40.dp)
|
||||
) {
|
||||
it.setAvatarUsingProfile(callParticipant.recipient)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
|
||||
Text(
|
||||
text = recipient.getShortDisplayName(LocalContext.current),
|
||||
text = callParticipant.recipient.getShortDisplayName(LocalContext.current),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
|
||||
if (isSelfAdmin) {
|
||||
if (isSelfAdmin && !callParticipant.recipient.isSelf) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_minus_circle_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { onBlockClicked(recipient) })
|
||||
.clickable(onClick = { onBlockClicked(callParticipant) })
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,8 @@ data class CallParticipantsState(
|
||||
val isInOutgoingRingingMode: Boolean = false,
|
||||
val ringGroup: Boolean = false,
|
||||
val ringerRecipient: Recipient = Recipient.UNKNOWN,
|
||||
val groupMembers: List<GroupMemberEntry.FullMember> = emptyList()
|
||||
val groupMembers: List<GroupMemberEntry.FullMember> = emptyList(),
|
||||
val isCallLink: Boolean = false
|
||||
) {
|
||||
|
||||
val allRemoteParticipants: List<CallParticipant> = remoteParticipants.allParticipants
|
||||
@@ -80,6 +81,7 @@ data class CallParticipantsState(
|
||||
return if (remoteParticipants.isEmpty) {
|
||||
describeGroupMembers(
|
||||
context = context,
|
||||
noParticipants = if (isCallLink) R.string.WebRtcCallView__signal_call_link else null,
|
||||
oneParticipant = if (ringGroup) R.string.WebRtcCallView__signal_will_ring_s else R.string.WebRtcCallView__s_will_be_notified,
|
||||
twoParticipants = if (ringGroup) R.string.WebRtcCallView__signal_will_ring_s_and_s else R.string.WebRtcCallView__s_and_s_will_be_notified,
|
||||
multipleParticipants = if (ringGroup) R.plurals.WebRtcCallView__signal_will_ring_s_s_and_d_others else R.plurals.WebRtcCallView__s_s_and_d_others_will_be_notified,
|
||||
@@ -115,6 +117,7 @@ data class CallParticipantsState(
|
||||
) {
|
||||
return describeGroupMembers(
|
||||
context = context,
|
||||
noParticipants = null,
|
||||
oneParticipant = R.string.WebRtcCallView__ringing_s,
|
||||
twoParticipants = R.string.WebRtcCallView__ringing_s_and_s,
|
||||
multipleParticipants = R.plurals.WebRtcCallView__ringing_s_s_and_d_others,
|
||||
@@ -223,7 +226,8 @@ data class CallParticipantsState(
|
||||
remoteDevicesCount = webRtcViewModel.remoteDevicesCount,
|
||||
ringGroup = webRtcViewModel.ringGroup,
|
||||
isInOutgoingRingingMode = isInOutgoingRingingMode,
|
||||
ringerRecipient = webRtcViewModel.ringerRecipient
|
||||
ringerRecipient = webRtcViewModel.ringerRecipient,
|
||||
isCallLink = webRtcViewModel.isCallLink
|
||||
)
|
||||
}
|
||||
|
||||
@@ -315,6 +319,7 @@ data class CallParticipantsState(
|
||||
|
||||
private fun describeGroupMembers(
|
||||
context: Context,
|
||||
@StringRes noParticipants: Int?,
|
||||
@StringRes oneParticipant: Int,
|
||||
@StringRes twoParticipants: Int,
|
||||
@PluralsRes multipleParticipants: Int,
|
||||
@@ -323,7 +328,7 @@ data class CallParticipantsState(
|
||||
val eligibleMembers: List<GroupMemberEntry.FullMember> = members.filterNot { it.member.isSelf || it.member.isBlocked }
|
||||
|
||||
return when (eligibleMembers.size) {
|
||||
0 -> ""
|
||||
0 -> noParticipants?.let { context.getString(noParticipants) } ?: ""
|
||||
1 -> context.getString(
|
||||
oneParticipant,
|
||||
eligibleMembers[0].member.getShortDisplayName(context)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc
|
||||
|
||||
/**
|
||||
* Data interface for the in-call status text to be displayed while a call
|
||||
* is ongoing.
|
||||
*/
|
||||
sealed interface InCallStatus {
|
||||
/**
|
||||
* The elapsed time the call has been connected for.
|
||||
*/
|
||||
data class ElapsedTime(val elapsedTime: Long) : InCallStatus
|
||||
|
||||
/**
|
||||
* The number of users requesting to join a call.
|
||||
*/
|
||||
data class PendingUsers(val pendingUserCount: Int) : InCallStatus
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -60,6 +61,13 @@ class PendingParticipantsBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
companion object {
|
||||
const val REQUEST_KEY = "PendingParticipantsBottomSheet_result"
|
||||
private const val ACTION = "PendingParticipantsBottomSheet_action"
|
||||
|
||||
@JvmStatic
|
||||
fun getAction(bundle: Bundle): Action {
|
||||
val code = bundle.getInt(ACTION, 0)
|
||||
return Action.values().first { it.code == code }
|
||||
}
|
||||
}
|
||||
|
||||
private val viewModel: WebRtcCallViewModel by activityViewModel {
|
||||
@@ -88,21 +96,39 @@ class PendingParticipantsBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
private fun onApprove(recipient: Recipient) {
|
||||
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(recipient)
|
||||
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(recipient.id)
|
||||
}
|
||||
|
||||
private fun onDeny(recipient: Recipient) {
|
||||
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(recipient)
|
||||
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(recipient.id)
|
||||
}
|
||||
|
||||
private fun onApproveAll() {
|
||||
dismiss()
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to true))
|
||||
|
||||
setFragmentResult(
|
||||
REQUEST_KEY,
|
||||
bundleOf(
|
||||
ACTION to Action.APPROVE_ALL.code
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun onDenyAll() {
|
||||
dismiss()
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to false))
|
||||
|
||||
setFragmentResult(
|
||||
REQUEST_KEY,
|
||||
bundleOf(
|
||||
ACTION to Action.DENY_ALL.code
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
enum class Action(val code: Int) {
|
||||
NONE(0),
|
||||
APPROVE_ALL(1),
|
||||
DENY_ALL(2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,8 +138,16 @@ private fun PendingParticipantsSheetPreview() {
|
||||
SignalTheme(isDarkMode = true) {
|
||||
Surface(shape = RoundedCornerShape(18.dp, 18.dp)) {
|
||||
PendingParticipantsSheet(
|
||||
pendingParticipants = (1 until 7).map {
|
||||
PendingParticipantCollection.Entry(Recipient.UNKNOWN, PendingParticipantCollection.State.PENDING, System.currentTimeMillis().milliseconds)
|
||||
pendingParticipants = listOf(
|
||||
PendingParticipantCollection.State.PENDING,
|
||||
PendingParticipantCollection.State.APPROVED,
|
||||
PendingParticipantCollection.State.DENIED
|
||||
).map {
|
||||
PendingParticipantCollection.Entry(
|
||||
recipient = Recipient.UNKNOWN,
|
||||
state = it,
|
||||
stateChangeAt = System.currentTimeMillis().milliseconds
|
||||
)
|
||||
},
|
||||
onApproveAll = {},
|
||||
onDenyAll = {},
|
||||
@@ -232,6 +266,11 @@ private fun PendingParticipantRow(
|
||||
symbol = ImageVector.vectorResource(id = R.drawable.symbol_x_compact_bold_16),
|
||||
contentDescription = stringResource(id = R.string.PendingParticipantsBottomSheet__reject),
|
||||
backgroundColor = colorResource(id = R.color.webrtc_hangup_background),
|
||||
state = when (participant.state) {
|
||||
PendingParticipantCollection.State.PENDING -> CircularIconButtonState.NORMAL
|
||||
PendingParticipantCollection.State.APPROVED -> CircularIconButtonState.INVISIBLE
|
||||
PendingParticipantCollection.State.DENIED -> CircularIconButtonState.DISABLED
|
||||
},
|
||||
onClick = onDenyCallback
|
||||
)
|
||||
|
||||
@@ -241,6 +280,11 @@ private fun PendingParticipantRow(
|
||||
symbol = ImageVector.vectorResource(id = R.drawable.symbol_check_compact_bold_16),
|
||||
contentDescription = stringResource(id = R.string.PendingParticipantsBottomSheet__approve),
|
||||
backgroundColor = colorResource(id = R.color.signal_accent_green),
|
||||
state = when (participant.state) {
|
||||
PendingParticipantCollection.State.PENDING -> CircularIconButtonState.NORMAL
|
||||
PendingParticipantCollection.State.APPROVED -> CircularIconButtonState.DISABLED
|
||||
PendingParticipantCollection.State.DENIED -> CircularIconButtonState.INVISIBLE
|
||||
},
|
||||
onClick = onApproveCallback
|
||||
)
|
||||
}
|
||||
@@ -251,20 +295,27 @@ private fun CircularIconButton(
|
||||
symbol: ImageVector,
|
||||
contentDescription: String?,
|
||||
backgroundColor: Color,
|
||||
state: CircularIconButtonState,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Icon(
|
||||
imageVector = symbol,
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier
|
||||
.size(28.dp)
|
||||
.background(
|
||||
color = backgroundColor,
|
||||
shape = CircleShape
|
||||
)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(6.dp)
|
||||
)
|
||||
if (state == CircularIconButtonState.INVISIBLE) {
|
||||
Spacer(modifier = Modifier.size(28.dp))
|
||||
} else {
|
||||
val enabled = state != CircularIconButtonState.DISABLED
|
||||
|
||||
Icon(
|
||||
imageVector = symbol,
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier
|
||||
.size(28.dp)
|
||||
.background(
|
||||
color = if (enabled) backgroundColor else MaterialTheme.colorScheme.secondaryContainer,
|
||||
shape = CircleShape
|
||||
)
|
||||
.clickable(enabled = enabled, onClick = onClick)
|
||||
.padding(6.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -289,3 +340,9 @@ private fun PendingParticipantAvatar(recipient: Recipient) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class CircularIconButtonState {
|
||||
NORMAL,
|
||||
DISABLED,
|
||||
INVISIBLE
|
||||
}
|
||||
|
||||
@@ -129,6 +129,7 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
private Toolbar collapsedToolbar;
|
||||
private Toolbar headerToolbar;
|
||||
private Stub<PendingParticipantsView> pendingParticipantsViewStub;
|
||||
private Stub<View> callLinkWarningCard;
|
||||
|
||||
private WebRtcCallParticipantsPagerAdapter pagerAdapter;
|
||||
private WebRtcCallParticipantsRecyclerAdapter recyclerAdapter;
|
||||
@@ -207,6 +208,7 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
collapsedToolbar = findViewById(R.id.webrtc_call_view_toolbar_text);
|
||||
headerToolbar = findViewById(R.id.webrtc_call_view_toolbar_no_text);
|
||||
pendingParticipantsViewStub = new Stub<>(findViewById(R.id.call_screen_pending_recipients));
|
||||
callLinkWarningCard = new Stub<>(findViewById(R.id.call_screen_call_link_warning));
|
||||
|
||||
View decline = findViewById(R.id.call_screen_decline_call);
|
||||
View answerLabel = findViewById(R.id.call_screen_answer_call_label);
|
||||
@@ -462,10 +464,13 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
|
||||
if (state.getGroupCallState().isNotIdle()) {
|
||||
if (state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
|
||||
callLinkWarningCard.setVisibility(callParticipantsViewState.isStartedFromCallLink() ? View.VISIBLE : View.GONE);
|
||||
setStatus(state.getPreJoinGroupDescription(getContext()));
|
||||
} else if (state.getCallState() == WebRtcViewModel.State.CALL_CONNECTED && state.isInOutgoingRingingMode()) {
|
||||
callLinkWarningCard.setVisibility(View.GONE);
|
||||
setStatus(state.getOutgoingRingingGroupDescription(getContext()));
|
||||
} else if (state.getGroupCallState().isRinging()) {
|
||||
callLinkWarningCard.setVisibility(View.GONE);
|
||||
setStatus(state.getIncomingRingingGroupDescription(getContext()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
private final LiveData<WebRtcControls> controlsWithFoldableState = LiveDataUtil.combineLatest(foldableState, webRtcControls, this::updateControlsFoldableState);
|
||||
private final LiveData<WebRtcControls> realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, controlsWithFoldableState, this::getRealWebRtcControls);
|
||||
private final SingleLiveEvent<Event> events = new SingleLiveEvent<>();
|
||||
private final MutableLiveData<Long> elapsed = new MutableLiveData<>(-1L);
|
||||
private final BehaviorSubject<Long> elapsed = BehaviorSubject.createDefault(-1L);
|
||||
private final MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
|
||||
private final DefaultValueLiveData<CallParticipantsState> participantsState = new DefaultValueLiveData<>(CallParticipantsState.STARTING_STATE);
|
||||
private final SingleLiveEvent<CallParticipantListUpdate> callParticipantListUpdate = new SingleLiveEvent<>();
|
||||
@@ -142,8 +142,22 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
return events;
|
||||
}
|
||||
|
||||
public LiveData<Long> getCallTime() {
|
||||
return Transformations.map(elapsed, timeInCall -> callConnectedTime == -1 ? -1 : timeInCall);
|
||||
public Observable<InCallStatus> getInCallstatus() {
|
||||
Observable<Long> elapsedTime = elapsed.map(timeInCall -> callConnectedTime == -1 ? -1 : timeInCall);
|
||||
|
||||
return Observable.combineLatest(
|
||||
elapsedTime,
|
||||
pendingParticipants,
|
||||
(time, participants) -> {
|
||||
Set<PendingParticipantCollection.Entry> pending = participants.getUnresolvedPendingParticipants();
|
||||
|
||||
if (pending.isEmpty()) {
|
||||
return new InCallStatus.ElapsedTime(time);
|
||||
} else {
|
||||
return new InCallStatus.PendingUsers(pending.size());
|
||||
}
|
||||
}
|
||||
).distinctUntilChanged();
|
||||
}
|
||||
|
||||
public LiveData<CallParticipantsState> getCallParticipantsState() {
|
||||
@@ -194,6 +208,10 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
return pendingParticipants.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
public @NonNull PendingParticipantCollection getPendingParticipantsSnapshot() {
|
||||
return pendingParticipants.getValue();
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void setIsInPipMode(boolean isInPipMode) {
|
||||
this.isInPipMode.setValue(isInPipMode);
|
||||
@@ -466,7 +484,7 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
|
||||
long newValue = (System.currentTimeMillis() - callConnectedTime) / 1000;
|
||||
|
||||
elapsed.postValue(newValue);
|
||||
elapsed.onNext(newValue);
|
||||
|
||||
elapsedTimeHandler.postDelayed(elapsedTimeRunnable, 1000);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package org.thoughtcrime.securesms.events;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.os.ParcelCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
@@ -11,7 +15,7 @@ import java.util.Objects;
|
||||
* Allow system to identify a call participant by their device demux id and their
|
||||
* recipient id.
|
||||
*/
|
||||
public final class CallParticipantId {
|
||||
public final class CallParticipantId implements Parcelable {
|
||||
|
||||
public static final long DEFAULT_ID = -1;
|
||||
|
||||
@@ -48,4 +52,34 @@ public final class CallParticipantId {
|
||||
public int hashCode() {
|
||||
return Objects.hash(demuxId, recipientId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
dest.writeLong(demuxId);
|
||||
dest.writeParcelable(recipientId, flags);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<CallParticipantId> CREATOR = new Parcelable.Creator<CallParticipantId>() {
|
||||
@Override
|
||||
public CallParticipantId createFromParcel(Parcel in) {
|
||||
return new CallParticipantId(
|
||||
in.readLong(),
|
||||
Objects.requireNonNull(
|
||||
ParcelCompat.readParcelable(in,
|
||||
RecipientId.class.getClassLoader(),
|
||||
RecipientId.class)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallParticipantId[] newArray(int size) {
|
||||
return new CallParticipantId[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ class WebRtcViewModel(state: WebRtcServiceState) {
|
||||
val remoteDevicesCount: OptionalLong = state.callInfoState.remoteDevicesCount
|
||||
val participantLimit: Long? = state.callInfoState.participantLimit
|
||||
val pendingParticipants: PendingParticipantCollection = state.callInfoState.pendingParticipants
|
||||
val isCallLink: Boolean = state.callInfoState.callRecipient.isCallLink
|
||||
|
||||
@get:JvmName("shouldRingGroup")
|
||||
val ringGroup: Boolean = state.getCallSetupState(state.callInfoState.activePeer?.callId).ringGroup
|
||||
|
||||
@@ -11,7 +11,9 @@ import org.signal.ringrtc.GroupCall
|
||||
import org.signal.ringrtc.PeekInfo
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.events.CallParticipant
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
@@ -58,18 +60,19 @@ class CallLinkConnectedActionProcessor(
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun handleSetCallLinkJoinRequestAccepted(currentState: WebRtcServiceState, participant: Recipient): WebRtcServiceState {
|
||||
override fun handleSetCallLinkJoinRequestAccepted(currentState: WebRtcServiceState, participant: RecipientId): WebRtcServiceState {
|
||||
Log.i(tag, "handleSetCallLinkJoinRequestAccepted():")
|
||||
|
||||
val groupCall: GroupCall = currentState.callInfoState.requireGroupCall()
|
||||
val recipient = Recipient.resolved(participant)
|
||||
|
||||
return try {
|
||||
groupCall.approveUser(participant.requireAci().rawUuid)
|
||||
groupCall.approveUser(recipient.requireAci().rawUuid)
|
||||
|
||||
currentState
|
||||
.builder()
|
||||
.changeCallInfoState()
|
||||
.setPendingParticipantApproved(participant)
|
||||
.setPendingParticipantApproved(recipient)
|
||||
.build()
|
||||
} catch (e: CallException) {
|
||||
Log.w(tag, "Failed to approve user.", e)
|
||||
@@ -78,22 +81,51 @@ class CallLinkConnectedActionProcessor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleSetCallLinkJoinRequestRejected(currentState: WebRtcServiceState, participant: Recipient): WebRtcServiceState {
|
||||
override fun handleSetCallLinkJoinRequestRejected(currentState: WebRtcServiceState, participant: RecipientId): WebRtcServiceState {
|
||||
Log.i(tag, "handleSetCallLinkJoinRequestRejected():")
|
||||
|
||||
val groupCall: GroupCall = currentState.callInfoState.requireGroupCall()
|
||||
val recipient = Recipient.resolved(participant)
|
||||
|
||||
return try {
|
||||
groupCall.denyUser(participant.requireAci().rawUuid)
|
||||
groupCall.denyUser(recipient.requireAci().rawUuid)
|
||||
|
||||
currentState
|
||||
.builder()
|
||||
.changeCallInfoState()
|
||||
.setPendingParticipantRejected(participant)
|
||||
.setPendingParticipantRejected(recipient)
|
||||
.build()
|
||||
} catch (e: CallException) {
|
||||
Log.w(tag, "Failed to deny user.", e)
|
||||
currentState
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleRemoveFromCallLink(currentState: WebRtcServiceState, participant: CallParticipant): WebRtcServiceState {
|
||||
Log.i(tag, "handleRemoveFromCallLink():")
|
||||
|
||||
val groupCall: GroupCall = currentState.callInfoState.requireGroupCall()
|
||||
|
||||
try {
|
||||
groupCall.removeClient(participant.callParticipantId.demuxId)
|
||||
} catch (e: CallException) {
|
||||
Log.w(tag, "Failed to remove user.", e)
|
||||
}
|
||||
|
||||
return currentState
|
||||
}
|
||||
|
||||
override fun handleBlockFromCallLink(currentState: WebRtcServiceState, participant: CallParticipant): WebRtcServiceState {
|
||||
Log.i(tag, "handleBlockFromCallLink():")
|
||||
|
||||
val groupCall: GroupCall = currentState.callInfoState.requireGroupCall()
|
||||
|
||||
try {
|
||||
groupCall.blockClient(participant.callParticipantId.demuxId)
|
||||
} catch (e: CallException) {
|
||||
Log.w(tag, "Failed to block user.", e)
|
||||
}
|
||||
|
||||
return currentState
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,17 @@ data class PendingParticipantCollection(
|
||||
private val nowProvider: () -> Duration = { System.currentTimeMillis().milliseconds }
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val MAX_DENIALS = 2
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new collection with the given recipients applied to it with the following rules:
|
||||
*
|
||||
* 1. If the recipient is already in the collection, ignore it
|
||||
* 1. Otherwise, insert the recipient at the end of the colleciton in the pending state
|
||||
* 1. Otherwise, insert the recipient at the end of the collection in the pending state
|
||||
* 1. Any recipients in the resulting collection that are [State.PENDING] and NOT in the passed recipient list are removed.
|
||||
* 1. Any recipients in the resulting collection that are [State.DENIED] and have a denial count less than [MAX_DENIALS] is moved to [State.PENDING]
|
||||
*/
|
||||
fun withRecipients(recipients: List<Recipient>): PendingParticipantCollection {
|
||||
val now = nowProvider()
|
||||
@@ -38,25 +43,36 @@ data class PendingParticipantCollection(
|
||||
)
|
||||
}
|
||||
|
||||
val recipientIds = recipients.map { it.id }
|
||||
val newEntryMap = (participantMap + newEntries).filterNot { it.value.state == State.PENDING && it.key !in recipientIds }
|
||||
val submittedIdSet = recipients.map { it.id }.toSet()
|
||||
val newEntryMap = (participantMap + newEntries)
|
||||
.filterNot { it.value.state == State.PENDING && it.key !in submittedIdSet }
|
||||
.mapValues {
|
||||
if (it.value.state == State.DENIED && it.key in submittedIdSet && it.value.denialCount < MAX_DENIALS) {
|
||||
it.value.copy(state = State.PENDING, stateChangeAt = now)
|
||||
} else {
|
||||
it.value
|
||||
}
|
||||
}
|
||||
|
||||
return copy(participantMap = newEntryMap)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new collection with the given recipient marked as [State.APPROVED]
|
||||
* Creates a new collection with the given recipient marked as [State.APPROVED].
|
||||
* Resets the denial count for that recipient.
|
||||
*/
|
||||
fun withApproval(recipient: Recipient): PendingParticipantCollection {
|
||||
val now = nowProvider()
|
||||
val entry = Entry(
|
||||
recipient = recipient,
|
||||
state = State.APPROVED,
|
||||
stateChangeAt = now
|
||||
)
|
||||
val entry = participantMap[recipient.id] ?: return this
|
||||
|
||||
return copy(
|
||||
participantMap = participantMap + (recipient.id to entry)
|
||||
participantMap = participantMap + (
|
||||
recipient.id to entry.copy(
|
||||
denialCount = 0,
|
||||
state = State.APPROVED,
|
||||
stateChangeAt = now
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -65,14 +81,16 @@ data class PendingParticipantCollection(
|
||||
*/
|
||||
fun withDenial(recipient: Recipient): PendingParticipantCollection {
|
||||
val now = nowProvider()
|
||||
val entry = Entry(
|
||||
recipient = recipient,
|
||||
state = State.DENIED,
|
||||
stateChangeAt = now
|
||||
)
|
||||
val entry = participantMap[recipient.id] ?: return this
|
||||
|
||||
return copy(
|
||||
participantMap = participantMap + (recipient.id to entry)
|
||||
participantMap = participantMap + (
|
||||
recipient.id to entry.copy(
|
||||
denialCount = entry.denialCount + 1,
|
||||
state = State.DENIED,
|
||||
stateChangeAt = now
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -103,7 +121,8 @@ data class PendingParticipantCollection(
|
||||
data class Entry(
|
||||
val recipient: Recipient,
|
||||
val state: State,
|
||||
val stateChangeAt: Duration
|
||||
val stateChangeAt: Duration,
|
||||
val denialCount: Int = 0
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,6 +40,7 @@ import org.thoughtcrime.securesms.database.GroupTable;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.GroupCallPeekEvent;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
@@ -354,19 +355,19 @@ private void processStateless(@NonNull Function1<WebRtcEphemeralState, WebRtcEph
|
||||
process((s, p) -> p.handleDropCall(s, callId));
|
||||
}
|
||||
|
||||
public void setCallLinkJoinRequestAccepted(@NonNull Recipient participant) {
|
||||
public void setCallLinkJoinRequestAccepted(@NonNull RecipientId participant) {
|
||||
process((s, p) -> p.handleSetCallLinkJoinRequestAccepted(s, participant));
|
||||
}
|
||||
|
||||
public void setCallLinkJoinRequestRejected(@NonNull Recipient participant) {
|
||||
public void setCallLinkJoinRequestRejected(@NonNull RecipientId participant) {
|
||||
process((s, p) -> p.handleSetCallLinkJoinRequestRejected(s, participant));
|
||||
}
|
||||
|
||||
public void removeFromCallLink(@NonNull Recipient participant) {
|
||||
public void removeFromCallLink(@NonNull CallParticipant participant) {
|
||||
process((s, p) -> p.handleRemoveFromCallLink(s, participant));
|
||||
}
|
||||
|
||||
public void blockFromCallLink(@NonNull Recipient participant) {
|
||||
public void blockFromCallLink(@NonNull CallParticipant participant) {
|
||||
process((s, p) -> p.handleBlockFromCallLink(s, participant));
|
||||
}
|
||||
|
||||
|
||||
@@ -877,25 +877,25 @@ public abstract class WebRtcActionProcessor {
|
||||
|
||||
//region Call Links
|
||||
|
||||
protected @NonNull WebRtcServiceState handleSetCallLinkJoinRequestAccepted(@NonNull WebRtcServiceState currentState, @NonNull Recipient participant) {
|
||||
protected @NonNull WebRtcServiceState handleSetCallLinkJoinRequestAccepted(@NonNull WebRtcServiceState currentState, @NonNull RecipientId participant) {
|
||||
Log.i(tag, "handleSetCallLinkJoinRequestAccepted not processed");
|
||||
|
||||
return currentState;
|
||||
}
|
||||
|
||||
protected @NonNull WebRtcServiceState handleSetCallLinkJoinRequestRejected(@NonNull WebRtcServiceState currentState, @NonNull Recipient participant) {
|
||||
protected @NonNull WebRtcServiceState handleSetCallLinkJoinRequestRejected(@NonNull WebRtcServiceState currentState, @NonNull RecipientId participant) {
|
||||
Log.i(tag, "handleSetCallLinkJoinRequestRejected not processed");
|
||||
|
||||
return currentState;
|
||||
}
|
||||
|
||||
protected @NonNull WebRtcServiceState handleRemoveFromCallLink(@NonNull WebRtcServiceState currentState, @NonNull Recipient participant) {
|
||||
protected @NonNull WebRtcServiceState handleRemoveFromCallLink(@NonNull WebRtcServiceState currentState, @NonNull CallParticipant participant) {
|
||||
Log.i(tag, "handleRemoveFromCallLink not processed");
|
||||
|
||||
return currentState;
|
||||
}
|
||||
|
||||
protected @NonNull WebRtcServiceState handleBlockFromCallLink(@NonNull WebRtcServiceState currentState, @NonNull Recipient participant) {
|
||||
protected @NonNull WebRtcServiceState handleBlockFromCallLink(@NonNull WebRtcServiceState currentState, @NonNull CallParticipant participant) {
|
||||
Log.i(tag, "handleBlockFromCallLink not processed");
|
||||
|
||||
return currentState;
|
||||
|
||||
@@ -84,11 +84,11 @@ public class CommunicationActions {
|
||||
@Override
|
||||
protected void onReceiveResult(int resultCode, Bundle resultData) {
|
||||
if (resultCode == 1) {
|
||||
startCallInternal(callContext, recipient, false);
|
||||
startCallInternal(callContext, recipient, false, false);
|
||||
} else {
|
||||
new MaterialAlertDialogBuilder(callContext.getContext())
|
||||
.setMessage(R.string.CommunicationActions_start_voice_call)
|
||||
.setPositiveButton(R.string.CommunicationActions_call, (d, w) -> startCallInternal(callContext, recipient, false))
|
||||
.setPositiveButton(R.string.CommunicationActions_call, (d, w) -> startCallInternal(callContext, recipient, false, false))
|
||||
.setNegativeButton(R.string.CommunicationActions_cancel, (d, w) -> d.dismiss())
|
||||
.setCancelable(true)
|
||||
.show();
|
||||
@@ -104,17 +104,17 @@ public class CommunicationActions {
|
||||
* Start a video call. Assumes that permission request results will be routed to a handler on the Fragment.
|
||||
*/
|
||||
public static void startVideoCall(@NonNull Fragment fragment, @NonNull Recipient recipient) {
|
||||
startVideoCall(new FragmentCallContext(fragment), recipient);
|
||||
startVideoCall(new FragmentCallContext(fragment), recipient, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a video call. Assumes that permission request results will be routed to a handler on the Activity.
|
||||
*/
|
||||
public static void startVideoCall(@NonNull Activity activity, @NonNull Recipient recipient) {
|
||||
startVideoCall(new ActivityCallContext(activity), recipient);
|
||||
startVideoCall(new ActivityCallContext(activity), recipient, false);
|
||||
}
|
||||
|
||||
private static void startVideoCall(@NonNull CallContext callContext, @NonNull Recipient recipient) {
|
||||
private static void startVideoCall(@NonNull CallContext callContext, @NonNull Recipient recipient, boolean fromCallLink) {
|
||||
if (TelephonyUtil.isAnyPstnLineBusy(callContext.getContext())) {
|
||||
Toast.makeText(callContext.getContext(),
|
||||
R.string.CommunicationActions_a_cellular_call_is_already_in_progress,
|
||||
@@ -126,7 +126,7 @@ public class CommunicationActions {
|
||||
ApplicationDependencies.getSignalCallManager().isCallActive(new ResultReceiver(new Handler(Looper.getMainLooper())) {
|
||||
@Override
|
||||
protected void onReceiveResult(int resultCode, Bundle resultData) {
|
||||
startCallInternal(callContext, recipient, resultCode != 1);
|
||||
startCallInternal(callContext, recipient, resultCode != 1, fromCallLink);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -377,7 +377,7 @@ public class CommunicationActions {
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
} else {
|
||||
startVideoCall(callContext, callLinkRecipient.get());
|
||||
startVideoCall(callContext, callLinkRecipient.get(), true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -394,8 +394,8 @@ public class CommunicationActions {
|
||||
}
|
||||
}
|
||||
|
||||
private static void startCallInternal(@NonNull CallContext callContext, @NonNull Recipient recipient, boolean isVideo) {
|
||||
if (isVideo) startVideoCallInternal(callContext, recipient);
|
||||
private static void startCallInternal(@NonNull CallContext callContext, @NonNull Recipient recipient, boolean isVideo, boolean fromCallLink) {
|
||||
if (isVideo) startVideoCallInternal(callContext, recipient, fromCallLink);
|
||||
else startAudioCallInternal(callContext, recipient);
|
||||
}
|
||||
|
||||
@@ -420,7 +420,7 @@ public class CommunicationActions {
|
||||
.execute();
|
||||
}
|
||||
|
||||
private static void startVideoCallInternal(@NonNull CallContext callContext, @NonNull Recipient recipient) {
|
||||
private static void startVideoCallInternal(@NonNull CallContext callContext, @NonNull Recipient recipient, boolean fromCallLink) {
|
||||
callContext.getPermissionsBuilder()
|
||||
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
@@ -434,7 +434,8 @@ public class CommunicationActions {
|
||||
Intent activityIntent = new Intent(callContext.getContext(), WebRtcCallActivity.class);
|
||||
|
||||
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra(WebRtcCallActivity.EXTRA_ENABLE_VIDEO_IF_AVAILABLE, true);
|
||||
.putExtra(WebRtcCallActivity.EXTRA_ENABLE_VIDEO_IF_AVAILABLE, true)
|
||||
.putExtra(WebRtcCallActivity.EXTRA_STARTED_FROM_CALL_LINK, fromCallLink);
|
||||
|
||||
callContext.startActivity(activityIntent);
|
||||
})
|
||||
|
||||
@@ -7,7 +7,8 @@ class CallParticipantsViewState(
|
||||
callParticipantsState: CallParticipantsState,
|
||||
ephemeralState: WebRtcEphemeralState,
|
||||
val isPortrait: Boolean,
|
||||
val isLandscapeEnabled: Boolean
|
||||
val isLandscapeEnabled: Boolean,
|
||||
val isStartedFromCallLink: Boolean
|
||||
) {
|
||||
|
||||
val callParticipantsState = CallParticipantsState.update(callParticipantsState, ephemeralState)
|
||||
|
||||
Reference in New Issue
Block a user