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

@@ -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))
}
}
}