Add snackbar that is displayed if you're currently in a different call.

This commit is contained in:
Alex Hart 2024-09-16 10:24:09 -03:00 committed by Greyson Parrelli
parent c36c6e62e2
commit 5bd3eda17d
24 changed files with 394 additions and 131 deletions

View file

@ -53,6 +53,7 @@ import org.signal.core.util.concurrent.LifecycleDisposable;
import org.signal.core.util.concurrent.RxExtensions;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar;
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
import org.thoughtcrime.securesms.contacts.ContactChipViewModel;
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
@ -1009,12 +1010,16 @@ public final class ContactSelectionListFragment extends LoggingFragment {
private class CallButtonClickCallbacks implements ContactSearchAdapter.CallButtonClickCallbacks {
@Override
public void onVideoCallButtonClicked(@NonNull Recipient recipient) {
CommunicationActions.startVideoCall(ContactSelectionListFragment.this, recipient);
CommunicationActions.startVideoCall(ContactSelectionListFragment.this, recipient, () -> {
YouAreAlreadyInACallSnackbar.show(requireView());
});
}
@Override
public void onAudioCallButtonClicked(@NonNull Recipient recipient) {
CommunicationActions.startVoiceCall(ContactSelectionListFragment.this, recipient);
CommunicationActions.startVoiceCall(ContactSelectionListFragment.this, recipient, () -> {
YouAreAlreadyInACallSnackbar.show(requireView());
});
}
}

View file

@ -18,6 +18,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.signal.donations.StripeApi;
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar;
import org.thoughtcrime.securesms.components.DebugLogsPromptDialogFragment;
import org.thoughtcrime.securesms.components.DeviceSpecificNotificationBottomSheet;
import org.thoughtcrime.securesms.components.PromptBatterySaverDialogFragment;
@ -243,7 +244,9 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
private void handleCallLinkInIntent(Intent intent) {
Uri data = intent.getData();
if (data != null) {
CommunicationActions.handlePotentialCallLinkUrl(this, data.toString());
CommunicationActions.handlePotentialCallLinkUrl(this, data.toString(), () -> {
YouAreAlreadyInACallSnackbar.show(findViewById(android.R.id.content));
});
}
}

View file

@ -39,6 +39,7 @@ import org.signal.core.util.DimensionUnit;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar;
import org.thoughtcrime.securesms.components.menu.ActionItem;
import org.thoughtcrime.securesms.components.menu.SignalContextMenu;
import org.thoughtcrime.securesms.contacts.management.ContactsManagementRepository;
@ -305,7 +306,9 @@ public class NewConversationActivity extends ContactSelectionActivity
R.drawable.ic_phone_right_24,
getString(R.string.NewConversationActivity__audio_call),
R.color.signal_colorOnSurface,
() -> CommunicationActions.startVoiceCall(this, recipient)
() -> CommunicationActions.startVoiceCall(this, recipient, () -> {
YouAreAlreadyInACallSnackbar.show(findViewById(android.R.id.content));
})
);
} else {
return null;
@ -321,7 +324,9 @@ public class NewConversationActivity extends ContactSelectionActivity
R.drawable.ic_video_call_24,
getString(R.string.NewConversationActivity__video_call),
R.color.signal_colorOnSurface,
() -> CommunicationActions.startVideoCall(this, recipient)
() -> CommunicationActions.startVideoCall(this, recipient, () -> {
YouAreAlreadyInACallSnackbar.show(findViewById(android.R.id.content));
})
);
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.calls
import android.view.View
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.google.android.material.snackbar.Snackbar
import org.signal.core.ui.Snackbars
import org.thoughtcrime.securesms.R
/**
* Snackbar which can be displayed whenever the user tries to join a call but is already in another.
*/
object YouAreAlreadyInACallSnackbar {
/**
* Composable component
*/
@Composable
fun YouAreAlreadyInACallSnackbar(
displaySnackbar: Boolean,
modifier: Modifier = Modifier
) {
val message = stringResource(R.string.CommunicationActions__you_are_already_in_a_call)
val hostState = remember { SnackbarHostState() }
Snackbars.Host(hostState, modifier = modifier)
LaunchedEffect(displaySnackbar) {
if (displaySnackbar) {
hostState.showSnackbar(message)
}
}
}
/**
* View system component
*/
@JvmStatic
fun show(view: View) {
Snackbar.make(
view,
view.context.getString(R.string.CommunicationActions__you_are_already_in_a_call),
Snackbar.LENGTH_LONG
).show()
}
}

View file

@ -11,6 +11,7 @@ import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@ -21,6 +22,7 @@ import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -36,21 +38,28 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Dividers
import org.signal.core.ui.Previews
import org.signal.core.ui.Rows
import org.signal.core.ui.SignalPreview
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.logging.Log
import org.signal.ringrtc.CallLinkState
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.YouAreAlreadyInACallSnackbar
import org.thoughtcrime.securesms.calls.links.CallLinks
import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment
import org.thoughtcrime.securesms.calls.links.SignalCallRow
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.service.webrtc.links.CreateCallLinkResult
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
import org.thoughtcrime.securesms.sharing.v2.ShareActivity
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.Util
import java.time.Instant
/**
* Bottom sheet for creating call links
@ -77,84 +86,21 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
@Composable
override fun SheetContent() {
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentSize(Alignment.Center)
) {
val callLink: CallLinkTable.CallLink by viewModel.callLink
val callLink: CallLinkTable.CallLink by viewModel.callLink
val displayAlreadyInACallSnackbar: Boolean by viewModel.showAlreadyInACall.collectAsState(false)
BottomSheets.Handle(modifier = Modifier.align(Alignment.CenterHorizontally))
Spacer(modifier = Modifier.height(20.dp))
Text(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__create_call_link),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
SignalCallRow(
callLink = callLink,
callLinkPeekInfo = null,
onJoinClicked = this@CreateCallLinkBottomSheetDialogFragment::onJoinClicked
)
Spacer(modifier = Modifier.height(12.dp))
Rows.TextRow(
text = stringResource(
id = if (callLink.state.name.isEmpty()) {
R.string.CreateCallLinkBottomSheetDialogFragment__add_call_name
} else {
R.string.CreateCallLinkBottomSheetDialogFragment__edit_call_name
}
),
onClick = this@CreateCallLinkBottomSheetDialogFragment::onAddACallNameClicked
)
Rows.ToggleRow(
checked = callLink.state.restrictions == CallLinkState.Restrictions.ADMIN_APPROVAL,
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__require_admin_approval),
onCheckChanged = this@CreateCallLinkBottomSheetDialogFragment::setApproveAllMembers,
modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::toggleApproveAllMembers)
)
Dividers.Default()
Rows.TextRow(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link_via_signal),
icon = painterResource(id = R.drawable.symbol_forward_24),
onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareViaSignalClicked
)
Rows.TextRow(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__copy_link),
icon = painterResource(id = R.drawable.symbol_copy_android_24),
onClick = this@CreateCallLinkBottomSheetDialogFragment::onCopyLinkClicked
)
Rows.TextRow(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link),
icon = painterResource(id = R.drawable.symbol_share_android_24),
onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareLinkClicked
)
Buttons.MediumTonal(
onClick = this@CreateCallLinkBottomSheetDialogFragment::onDoneClicked,
modifier = Modifier
.padding(end = dimensionResource(id = R.dimen.core_ui__gutter))
.align(Alignment.End)
) {
Text(text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__done))
}
Spacer(modifier = Modifier.size(16.dp))
}
CreateCallLinkBottomSheetContent(
callLink = callLink,
onJoinClicked = this@CreateCallLinkBottomSheetDialogFragment::onJoinClicked,
onAddACallNameClicked = this@CreateCallLinkBottomSheetDialogFragment::onAddACallNameClicked,
onApproveAllMembersChanged = this@CreateCallLinkBottomSheetDialogFragment::setApproveAllMembers,
onToggleApproveAllMembersClicked = this@CreateCallLinkBottomSheetDialogFragment::toggleApproveAllMembers,
onShareViaSignalClicked = this@CreateCallLinkBottomSheetDialogFragment::onShareViaSignalClicked,
onCopyLinkClicked = this@CreateCallLinkBottomSheetDialogFragment::onCopyLinkClicked,
onShareLinkClicked = this@CreateCallLinkBottomSheetDialogFragment::onShareLinkClicked,
onDoneClicked = this@CreateCallLinkBottomSheetDialogFragment::onDoneClicked,
displayAlreadyInACallSnackbar = displayAlreadyInACallSnackbar
)
}
private fun setCallName(callName: String) {
@ -195,7 +141,9 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
lifecycleDisposable += viewModel.commitCallLink().subscribeBy(onSuccess = {
when (it) {
is EnsureCallLinkCreatedResult.Success -> {
CommunicationActions.startVideoCall(requireActivity(), it.recipient)
CommunicationActions.startVideoCall(requireActivity(), it.recipient) {
viewModel.setShowAlreadyInACall(true)
}
dismissAllowingStateLoss()
}
@ -282,3 +230,123 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
Toast.makeText(requireContext(), R.string.CallLinkDetailsFragment__couldnt_save_changes, Toast.LENGTH_LONG).show()
}
}
@Composable
private fun CreateCallLinkBottomSheetContent(
callLink: CallLinkTable.CallLink,
displayAlreadyInACallSnackbar: Boolean,
onJoinClicked: () -> Unit = {},
onAddACallNameClicked: () -> Unit = {},
onApproveAllMembersChanged: (Boolean) -> Unit = {},
onToggleApproveAllMembersClicked: () -> Unit = {},
onShareViaSignalClicked: () -> Unit = {},
onCopyLinkClicked: () -> Unit = {},
onShareLinkClicked: () -> Unit = {},
onDoneClicked: () -> Unit = {}
) {
Box {
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentSize(Alignment.Center)
) {
BottomSheets.Handle(modifier = Modifier.align(Alignment.CenterHorizontally))
Spacer(modifier = Modifier.height(20.dp))
Text(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__create_call_link),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
SignalCallRow(
callLink = callLink,
callLinkPeekInfo = null,
onJoinClicked = onJoinClicked
)
Spacer(modifier = Modifier.height(12.dp))
Rows.TextRow(
text = stringResource(
id = if (callLink.state.name.isEmpty()) {
R.string.CreateCallLinkBottomSheetDialogFragment__add_call_name
} else {
R.string.CreateCallLinkBottomSheetDialogFragment__edit_call_name
}
),
onClick = onAddACallNameClicked
)
Rows.ToggleRow(
checked = callLink.state.restrictions == CallLinkState.Restrictions.ADMIN_APPROVAL,
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__require_admin_approval),
onCheckChanged = onApproveAllMembersChanged,
modifier = Modifier.clickable(onClick = onToggleApproveAllMembersClicked)
)
Dividers.Default()
Rows.TextRow(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link_via_signal),
icon = painterResource(id = R.drawable.symbol_forward_24),
onClick = onShareViaSignalClicked
)
Rows.TextRow(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__copy_link),
icon = painterResource(id = R.drawable.symbol_copy_android_24),
onClick = onCopyLinkClicked
)
Rows.TextRow(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link),
icon = painterResource(id = R.drawable.symbol_share_android_24),
onClick = onShareLinkClicked
)
Buttons.MediumTonal(
onClick = onDoneClicked,
modifier = Modifier
.padding(end = dimensionResource(id = R.dimen.core_ui__gutter))
.align(Alignment.End)
) {
Text(text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__done))
}
Spacer(modifier = Modifier.size(16.dp))
}
YouAreAlreadyInACallSnackbar(
displaySnackbar = displayAlreadyInACallSnackbar,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
@SignalPreview
@Composable
private fun CreateCallLinkBottomSheetContentPreview() {
Previews.BottomSheetPreview {
CreateCallLinkBottomSheetContent(
callLink = CallLinkTable.CallLink(
recipientId = RecipientId.UNKNOWN,
roomId = CallLinkRoomId.fromBytes(byteArrayOf(1, 2, 3, 4)),
credentials = null,
state = SignalCallLinkState(
name = "Test Call",
restrictions = CallLinkState.Restrictions.ADMIN_APPROVAL,
revoked = false,
expiration = Instant.MAX
),
deletionTimestamp = 0L
),
displayAlreadyInACallSnackbar = true
)
}
}

View file

@ -14,6 +14,9 @@ import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.kotlin.subscribeBy
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import org.signal.ringrtc.CallLinkState.Restrictions
import org.thoughtcrime.securesms.calls.links.CallLinks
import org.thoughtcrime.securesms.calls.links.UpdateCallLinkRepository
@ -47,6 +50,9 @@ class CreateCallLinkViewModel(
val callLink: State<CallLinkTable.CallLink> = _callLink
val linkKeyBytes: ByteArray = credentials.linkKeyBytes
private val internalShowAlreadyInACall = MutableStateFlow(false)
val showAlreadyInACall: StateFlow<Boolean> = internalShowAlreadyInACall
private val disposables = CompositeDisposable()
init {
@ -61,6 +67,10 @@ class CreateCallLinkViewModel(
disposables.dispose()
}
fun setShowAlreadyInACall(showAlreadyInACall: Boolean) {
internalShowAlreadyInACall.update { showAlreadyInACall }
}
fun commitCallLink(): Single<EnsureCallLinkCreatedResult> {
return repository.ensureCallLinkCreated(credentials)
.observeOn(AndroidSchedulers.mainThread())

View file

@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@ -37,6 +38,8 @@ import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.logging.Log
import org.signal.ringrtc.CallLinkState.Restrictions
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.YouAreAlreadyInACallSnackbar
import org.thoughtcrime.securesms.calls.links.CallLinks
import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment
import org.thoughtcrime.securesms.calls.links.SignalCallRow
@ -44,6 +47,7 @@ import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
import org.thoughtcrime.securesms.sharing.v2.ShareActivity
@ -79,9 +83,11 @@ class CallLinkDetailsFragment : ComposeFragment(), CallLinkDetailsCallback {
@Composable
override fun FragmentContent() {
val state by viewModel.state
val showAlreadyInACall by viewModel.showAlreadyInACall.collectAsState(false)
CallLinkDetails(
state,
showAlreadyInACall,
this
)
}
@ -93,7 +99,9 @@ class CallLinkDetailsFragment : ComposeFragment(), CallLinkDetailsCallback {
override fun onJoinClicked() {
val recipientSnapshot = viewModel.recipientSnapshot
if (recipientSnapshot != null) {
CommunicationActions.startVideoCall(this, recipientSnapshot)
CommunicationActions.startVideoCall(this, recipientSnapshot) {
viewModel.showAlreadyInACall(true)
}
}
}
@ -206,7 +214,7 @@ private fun CallLinkDetailsPreview() {
)
CallLinkTable.CallLink(
recipientId = RecipientId.UNKNOWN,
roomId = credentials.roomId,
roomId = CallLinkRoomId.fromBytes(byteArrayOf(1, 2, 3, 4)),
credentials = credentials,
state = SignalCallLinkState(
name = "Call Name",
@ -224,6 +232,7 @@ private fun CallLinkDetailsPreview() {
false,
callLink
),
true,
object : CallLinkDetailsCallback {
override fun onDeleteConfirmed() = Unit
override fun onDeleteCanceled() = Unit
@ -243,10 +252,14 @@ private fun CallLinkDetailsPreview() {
@Composable
private fun CallLinkDetails(
state: CallLinkDetailsState,
showAlreadyInACall: Boolean,
callback: CallLinkDetailsCallback
) {
Scaffolds.Settings(
title = stringResource(id = R.string.CallLinkDetailsFragment__call_details),
snackbarHost = {
YouAreAlreadyInACallSnackbar(showAlreadyInACall)
},
onNavigationClick = callback::onNavigationClicked,
navigationIconPainter = painterResource(id = R.drawable.ic_arrow_left_24)
) { paddingValues ->

View file

@ -17,6 +17,9 @@ import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.BehaviorSubject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import org.signal.ringrtc.CallLinkState
import org.thoughtcrime.securesms.calls.links.CallLinks
import org.thoughtcrime.securesms.calls.links.UpdateCallLinkRepository
@ -44,6 +47,9 @@ class CallLinkDetailsViewModel(
val recipientSnapshot: Recipient?
get() = recipientSubject.value
private val internalShowAlreadyInACall = MutableStateFlow(false)
val showAlreadyInACall: StateFlow<Boolean> = internalShowAlreadyInACall
init {
disposables += repository.refreshCallLinkState(callLinkRoomId)
disposables += CallLinks.watchCallLink(callLinkRoomId)
@ -80,6 +86,10 @@ class CallLinkDetailsViewModel(
disposables.dispose()
}
fun showAlreadyInACall(showAlreadyInACall: Boolean) {
internalShowAlreadyInACall.update { showAlreadyInACall }
}
fun setDisplayRevocationDialog(displayRevocationDialog: Boolean) {
_state.value = _state.value.copy(displayRevocationDialog = displayRevocationDialog)
}

View file

@ -7,6 +7,7 @@ import androidx.recyclerview.widget.RecyclerView
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.concurrent.LifecycleDisposable
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsActivity
import org.thoughtcrime.securesms.components.menu.ActionItem
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
@ -72,7 +73,9 @@ class CallLogContextMenu(
iconRes = R.drawable.symbol_video_24,
title = fragment.getString(R.string.CallContextMenu__video_call)
) {
CommunicationActions.startVideoCall(fragment, peer)
CommunicationActions.startVideoCall(fragment, peer) {
YouAreAlreadyInACallSnackbar.show(fragment.requireView())
}
}
}
@ -85,7 +88,9 @@ class CallLogContextMenu(
iconRes = R.drawable.symbol_phone_24,
title = fragment.getString(R.string.CallContextMenu__audio_call)
) {
CommunicationActions.startVoiceCall(fragment, call.peer)
CommunicationActions.startVoiceCall(fragment, call.peer) {
YouAreAlreadyInACallSnackbar.show(fragment.requireView())
}
}
}

View file

@ -35,6 +35,7 @@ import org.signal.core.util.concurrent.addTo
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsActivity
import org.thoughtcrime.securesms.calls.new.NewCallActivity
import org.thoughtcrime.securesms.components.Material3SearchToolbar
@ -391,12 +392,16 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
}
override fun onStartAudioCallClicked(recipient: Recipient) {
CommunicationActions.startVoiceCall(this, recipient)
CommunicationActions.startVoiceCall(this, recipient) {
YouAreAlreadyInACallSnackbar.show(requireView())
}
}
override fun onStartVideoCallClicked(recipient: Recipient, canUserBeginCall: Boolean) {
if (canUserBeginCall) {
CommunicationActions.startVideoCall(this, recipient)
CommunicationActions.startVideoCall(this, recipient) {
YouAreAlreadyInACallSnackbar.show(requireView())
}
} else {
ConversationDialogs.displayCannotStartGroupCallDueToPermissionsDialog(requireContext())
}

View file

@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.ContactSelectionActivity
import org.thoughtcrime.securesms.ContactSelectionListFragment
import org.thoughtcrime.securesms.InviteActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
@ -81,9 +82,13 @@ class NewCallActivity : ContactSelectionActivity(), ContactSelectionListFragment
private fun launch(recipient: Recipient) {
if (recipient.isGroup) {
CommunicationActions.startVideoCall(this, recipient)
CommunicationActions.startVideoCall(this, recipient) {
YouAreAlreadyInACallSnackbar.show(findViewById(android.R.id.content))
}
} else {
CommunicationActions.startVoiceCall(this, recipient)
CommunicationActions.startVoiceCall(this, recipient) {
YouAreAlreadyInACallSnackbar.show(findViewById(android.R.id.content))
}
}
}

View file

@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.badges.Badges.displayBadges
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.view.ViewBadgeBottomSheetDialogFragment
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar
import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.components.recyclerview.OnScrollAnimationHelper
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
@ -442,11 +443,15 @@ class ConversationSettingsFragment : DSLSettingsFragment(
.setPositiveButton(android.R.string.ok) { d, _ -> d.dismiss() }
.show()
} else {
CommunicationActions.startVideoCall(requireActivity(), state.recipient)
CommunicationActions.startVideoCall(requireActivity(), state.recipient) {
YouAreAlreadyInACallSnackbar.show(requireView())
}
}
},
onAudioClick = {
CommunicationActions.startVoiceCall(requireActivity(), state.recipient)
CommunicationActions.startVoiceCall(requireActivity(), state.recipient) {
YouAreAlreadyInACallSnackbar.show(requireView())
}
},
onMuteClick = {
if (!state.buttonStripState.isMuted) {

View file

@ -24,6 +24,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar;
import org.thoughtcrime.securesms.database.RecipientTable;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
@ -214,7 +215,9 @@ public class SharedContactDetailsActivity extends PassphraseRequiredActivity {
});
callButtonView.setOnClickListener(v -> {
ContactUtil.selectRecipientThroughDialog(this, pushUsers, dynamicLanguage.getCurrentLocale(), recipient -> CommunicationActions.startVoiceCall(this, recipient));
ContactUtil.selectRecipientThroughDialog(this, pushUsers, dynamicLanguage.getCurrentLocale(), recipient -> CommunicationActions.startVoiceCall(this, recipient, () -> {
YouAreAlreadyInACallSnackbar.show(callButtonView);
}));
});
} else if (!systemUsers.isEmpty()) {
inviteButtonView.setVisibility(View.VISIBLE);

View file

@ -108,6 +108,7 @@ import org.thoughtcrime.securesms.badges.gifts.viewgift.received.ViewReceivedGif
import org.thoughtcrime.securesms.badges.gifts.viewgift.sent.ViewSentGiftBottomSheet
import org.thoughtcrime.securesms.banner.banners.ServiceOutageBanner
import org.thoughtcrime.securesms.banner.banners.UnauthorizedBanner
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar
import org.thoughtcrime.securesms.components.AnimatingToggle
import org.thoughtcrime.securesms.components.ComposeText
import org.thoughtcrime.securesms.components.ConversationSearchBottomBar
@ -1494,7 +1495,9 @@ class ConversationFragment :
private fun handleVideoCall() {
val recipient = viewModel.recipientSnapshot ?: return
if (!recipient.isGroup) {
CommunicationActions.startVideoCall(this, recipient)
CommunicationActions.startVideoCall(this, recipient) {
YouAreAlreadyInACallSnackbar.show(requireView())
}
return
}
@ -1510,7 +1513,9 @@ class ConversationFragment :
if (notAllowed) {
ConversationDialogs.displayCannotStartGroupCallDueToPermissionsDialog(requireContext())
} else {
CommunicationActions.startVideoCall(this, recipient)
CommunicationActions.startVideoCall(this, recipient) {
YouAreAlreadyInACallSnackbar.show(requireView())
}
}
}
}
@ -2962,7 +2967,9 @@ class ConversationFragment :
override fun onJoinGroupCallClicked() {
val activity = activity ?: return
val recipient = viewModel.recipientSnapshot ?: return
CommunicationActions.startVideoCall(activity, recipient)
CommunicationActions.startVideoCall(activity, recipient) {
YouAreAlreadyInACallSnackbar.show(requireView())
}
}
override fun onInviteFriendsToGroupClicked(groupId: GroupId.V2) {
@ -3280,7 +3287,9 @@ class ConversationFragment :
}
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) {
CommunicationActions.startVideoCall(this@ConversationFragment, callLinkRootKey)
CommunicationActions.startVideoCall(this@ConversationFragment, callLinkRootKey) {
YouAreAlreadyInACallSnackbar.show(requireView())
}
}
override fun onShowSafetyTips(forGroup: Boolean) {
@ -3420,7 +3429,9 @@ class ConversationFragment :
override fun handleDial() {
val recipient: Recipient = viewModel.recipientSnapshot ?: return
CommunicationActions.startVoiceCall(this@ConversationFragment, recipient)
CommunicationActions.startVoiceCall(this@ConversationFragment, recipient) {
YouAreAlreadyInACallSnackbar.show(requireView())
}
}
override fun handleViewMedia() {

View file

@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.avatar.view.AvatarView;
import org.thoughtcrime.securesms.badges.BadgeImageView;
import org.thoughtcrime.securesms.badges.view.ViewBadgeBottomSheetDialogFragment;
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar;
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon;
import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference;
import org.thoughtcrime.securesms.fonts.SignalSymbols;
@ -270,12 +271,12 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
return Unit.INSTANCE;
},
() -> {
viewModel.onSecureVideoCallClicked(requireActivity());
viewModel.onSecureVideoCallClicked(requireActivity(), () -> YouAreAlreadyInACallSnackbar.show(requireView()));
return Unit.INSTANCE;
},
() -> {
if (buttonStripState.isAudioSecure()) {
viewModel.onSecureCallClicked(requireActivity());
viewModel.onSecureCallClicked(requireActivity(), () -> YouAreAlreadyInACallSnackbar.show(requireView()));
} else {
viewModel.onInsecureCallClicked(requireActivity());
}

View file

@ -7,9 +7,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
@ -162,16 +160,16 @@ final class RecipientDialogViewModel extends ViewModel {
recipientDialogRepository.getRecipient(recipient -> CommunicationActions.startConversation(activity, recipient, null));
}
void onSecureCallClicked(@NonNull FragmentActivity activity) {
recipientDialogRepository.getRecipient(recipient -> CommunicationActions.startVoiceCall(activity, recipient));
void onSecureCallClicked(@NonNull FragmentActivity activity, @NonNull CommunicationActions.OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) {
recipientDialogRepository.getRecipient(recipient -> CommunicationActions.startVoiceCall(activity, recipient, onUserAlreadyInAnotherCall));
}
void onInsecureCallClicked(@NonNull FragmentActivity activity) {
recipientDialogRepository.getRecipient(recipient -> CommunicationActions.startInsecureCall(activity, recipient));
}
void onSecureVideoCallClicked(@NonNull FragmentActivity activity) {
recipientDialogRepository.getRecipient(recipient -> CommunicationActions.startVideoCall(activity, recipient));
void onSecureVideoCallClicked(@NonNull FragmentActivity activity, @NonNull CommunicationActions.OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) {
recipientDialogRepository.getRecipient(recipient -> CommunicationActions.startVideoCall(activity, recipient, onUserAlreadyInAnotherCall));
}
void onBlockClicked(@NonNull FragmentActivity activity) {

View file

@ -51,7 +51,7 @@ public class ActiveCallActionProcessorDelegate extends WebRtcActionProcessor {
@Override
protected @NonNull WebRtcServiceState handleIsInCallQuery(@NonNull WebRtcServiceState currentState, @Nullable ResultReceiver resultReceiver) {
if (resultReceiver != null) {
resultReceiver.send(1, null);
resultReceiver.send(1, ActiveCallData.fromCallState(currentState).toBundle());
}
return currentState;
}

View file

@ -0,0 +1,41 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.service.webrtc
import android.os.Bundle
import android.os.Parcelable
import androidx.core.os.bundleOf
import kotlinx.parcelize.Parcelize
import org.signal.core.util.getParcelableCompat
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState
/**
* Active call data to be returned from calls to isInCallQuery.
*/
@Parcelize
data class ActiveCallData(
val recipientId: RecipientId
) : Parcelable {
companion object {
private const val KEY = "ACTIVE_CALL_DATA"
@JvmStatic
fun fromCallState(webRtcServiceState: WebRtcServiceState): ActiveCallData {
return ActiveCallData(
webRtcServiceState.callInfoState.callRecipient.id
)
}
@JvmStatic
fun fromBundle(bundle: Bundle): ActiveCallData {
return bundle.getParcelableCompat(KEY, ActiveCallData::class.java)!!
}
}
fun toBundle(): Bundle = bundleOf(KEY to this)
}

View file

@ -49,7 +49,7 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor {
@Override
protected @NonNull WebRtcServiceState handleIsInCallQuery(@NonNull WebRtcServiceState currentState, @Nullable ResultReceiver resultReceiver) {
if (resultReceiver != null) {
resultReceiver.send(1, null);
resultReceiver.send(1, ActiveCallData.fromCallState(currentState).toBundle());
}
return currentState;
}

View file

@ -36,7 +36,7 @@ public class GroupJoiningActionProcessor extends GroupActionProcessor {
@Override
protected @NonNull WebRtcServiceState handleIsInCallQuery(@NonNull WebRtcServiceState currentState, @Nullable ResultReceiver resultReceiver) {
if (resultReceiver != null) {
resultReceiver.send(1, null);
resultReceiver.send(1, ActiveCallData.fromCallState(currentState).toBundle());
}
return currentState;
}

View file

@ -100,7 +100,7 @@ public abstract class WebRtcActionProcessor {
protected @NonNull WebRtcServiceState handleIsInCallQuery(@NonNull WebRtcServiceState currentState, @Nullable ResultReceiver resultReceiver) {
if (resultReceiver != null) {
resultReceiver.send(0, null);
resultReceiver.send(0, ActiveCallData.fromCallState(currentState).toBundle());
}
return currentState;
}

View file

@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.profiles.manage.UsernameRepository;
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameLinkConversionResult;
import org.thoughtcrime.securesms.proxy.ProxyBottomSheetFragment;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.webrtc.ActiveCallData;
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
@ -65,18 +66,18 @@ public class CommunicationActions {
/**
* Start a voice call. Assumes that permission request results will be routed to a handler on the Fragment.
*/
public static void startVoiceCall(@NonNull Fragment fragment, @NonNull Recipient recipient) {
startVoiceCall(new FragmentCallContext(fragment), recipient);
public static void startVoiceCall(@NonNull Fragment fragment, @NonNull Recipient recipient, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) {
startVoiceCall(new FragmentCallContext(fragment), recipient, onUserAlreadyInAnotherCall);
}
/**
* Start a voice call. Assumes that permission request results will be routed to a handler on the Activity.
*/
public static void startVoiceCall(@NonNull FragmentActivity activity, @NonNull Recipient recipient) {
startVoiceCall(new ActivityCallContext(activity), recipient);
public static void startVoiceCall(@NonNull FragmentActivity activity, @NonNull Recipient recipient, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) {
startVoiceCall(new ActivityCallContext(activity), recipient, onUserAlreadyInAnotherCall);
}
private static void startVoiceCall(@NonNull CallContext callContext, @NonNull Recipient recipient) {
private static void startVoiceCall(@NonNull CallContext callContext, @NonNull Recipient recipient, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) {
if (TelephonyUtil.isAnyPstnLineBusy(callContext.getContext())) {
Toast.makeText(callContext.getContext(),
R.string.CommunicationActions_a_cellular_call_is_already_in_progress,
@ -90,7 +91,12 @@ public class CommunicationActions {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
if (resultCode == 1) {
startCallInternal(callContext, recipient, false, false);
ActiveCallData activeCallData = ActiveCallData.fromBundle(resultData);
if (Objects.equals(activeCallData.getRecipientId(), recipient.getId())) {
startCallInternal(callContext, recipient, false, false);
} else {
onUserAlreadyInAnotherCall.onUserAlreadyInAnotherCall();
}
} else {
new MaterialAlertDialogBuilder(callContext.getContext())
.setMessage(R.string.CommunicationActions_start_voice_call)
@ -109,18 +115,18 @@ 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, false);
public static void startVideoCall(@NonNull Fragment fragment, @NonNull Recipient recipient, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) {
startVideoCall(new FragmentCallContext(fragment), recipient, false, onUserAlreadyInAnotherCall);
}
/**
* Start a video call. Assumes that permission request results will be routed to a handler on the Activity.
*/
public static void startVideoCall(@NonNull FragmentActivity activity, @NonNull Recipient recipient) {
startVideoCall(new ActivityCallContext(activity), recipient, false);
public static void startVideoCall(@NonNull FragmentActivity activity, @NonNull Recipient recipient, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) {
startVideoCall(new ActivityCallContext(activity), recipient, false, onUserAlreadyInAnotherCall);
}
private static void startVideoCall(@NonNull CallContext callContext, @NonNull Recipient recipient, boolean fromCallLink) {
private static void startVideoCall(@NonNull CallContext callContext, @NonNull Recipient recipient, boolean fromCallLink, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) {
if (TelephonyUtil.isAnyPstnLineBusy(callContext.getContext())) {
Toast.makeText(callContext.getContext(),
R.string.CommunicationActions_a_cellular_call_is_already_in_progress,
@ -132,7 +138,16 @@ public class CommunicationActions {
AppDependencies.getSignalCallManager().isCallActive(new ResultReceiver(new Handler(Looper.getMainLooper())) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
startCallInternal(callContext, recipient, resultCode != 1, fromCallLink);
if (resultCode == 1) {
ActiveCallData activeCallData = ActiveCallData.fromBundle(resultData);
if (Objects.equals(activeCallData.getRecipientId(), recipient.getId())) {
startCallInternal(callContext, recipient, false, fromCallLink);
} else {
onUserAlreadyInAnotherCall.onUserAlreadyInAnotherCall();
}
} else {
startCallInternal(callContext, recipient, true, fromCallLink);
}
}
});
}
@ -307,7 +322,7 @@ public class CommunicationActions {
}
}
public static void handlePotentialCallLinkUrl(@NonNull FragmentActivity activity, @NonNull String potentialUrl) {
public static void handlePotentialCallLinkUrl(@NonNull FragmentActivity activity, @NonNull String potentialUrl, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) {
if (!CallLinks.isCallLink(potentialUrl)) {
return;
}
@ -323,7 +338,7 @@ public class CommunicationActions {
return;
}
startVideoCall(new ActivityCallContext(activity), rootKey);
startVideoCall(new ActivityCallContext(activity), rootKey, onUserAlreadyInAnotherCall);
}
/**
@ -332,11 +347,11 @@ public class CommunicationActions {
*
* @param fragment The fragment, which will be used for context and permissions routing.
*/
public static void startVideoCall(@NonNull Fragment fragment, @NonNull CallLinkRootKey rootKey) {
startVideoCall(new FragmentCallContext(fragment), rootKey);
public static void startVideoCall(@NonNull Fragment fragment, @NonNull CallLinkRootKey rootKey, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) {
startVideoCall(new FragmentCallContext(fragment), rootKey, onUserAlreadyInAnotherCall);
}
private static void startVideoCall(@NonNull CallContext callContext, @NonNull CallLinkRootKey rootKey) {
private static void startVideoCall(@NonNull CallContext callContext, @NonNull CallLinkRootKey rootKey, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) {
SimpleTask.run(() -> {
CallLinkRoomId roomId = CallLinkRoomId.fromBytes(rootKey.deriveRoomId());
CallLinkTable.CallLink callLink = SignalDatabase.callLinks().getOrCreateCallLinkByRootKey(rootKey);
@ -354,7 +369,7 @@ public class CommunicationActions {
.setPositiveButton(android.R.string.ok, null)
.show();
} else {
startVideoCall(callContext, callLinkRecipient.get(), true);
startVideoCall(callContext, callLinkRecipient.get(), true, onUserAlreadyInAnotherCall);
}
});
}
@ -532,4 +547,8 @@ public class CommunicationActions {
return fragment.getParentFragmentManager();
}
}
public interface OnUserAlreadyInAnotherCall {
void onUserAlreadyInAnotherCall();
}
}

View file

@ -346,6 +346,8 @@
<string name="CommunicationActions_invalid_link">Invalid link</string>
<!-- Message on dialog when call link url cannot be parsed -->
<string name="CommunicationActions_this_is_not_a_valid_call_link">This is not a valid call link. Make sure the entire link is intact and correct before attempting to join.</string>
<!-- Displayed in a snackbar when the user is already in a call -->
<string name="CommunicationActions__you_are_already_in_a_call">You are already in a call</string>
<!-- ConfirmIdentityDialog -->

View file

@ -13,6 +13,7 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarVisuals
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import org.signal.core.ui.theme.LocalSnackbarColors
import org.signal.core.ui.theme.SignalTheme
@ -24,8 +25,8 @@ import org.signal.core.ui.theme.SignalTheme
*/
object Snackbars {
@Composable
fun Host(snackbarHostState: SnackbarHostState) {
SnackbarHost(hostState = snackbarHostState) {
fun Host(snackbarHostState: SnackbarHostState, modifier: Modifier = Modifier) {
SnackbarHost(hostState = snackbarHostState, modifier = modifier) {
Default(snackbarData = it)
}
}