Allow call links to exist in the calls tab.
This commit is contained in:
parent
97d95f37cc
commit
987f9b9dba
29 changed files with 657 additions and 117 deletions
|
@ -673,6 +673,11 @@
|
|||
android:windowSoftInputMode="stateAlwaysVisible"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".calls.links.details.CallLinkDetailsActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".calls.new.NewCallActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysVisible"
|
||||
|
|
|
@ -49,7 +49,9 @@ import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectFor
|
|||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CreateCallLinkResult
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareArgs
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
|
||||
/**
|
||||
|
@ -150,18 +152,30 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
|
|||
}
|
||||
|
||||
private fun setCallName(callName: String) {
|
||||
lifecycleDisposable += viewModel.setCallName(callName).subscribeBy {
|
||||
}
|
||||
lifecycleDisposable += viewModel.setCallName(callName).subscribeBy(onSuccess = {
|
||||
if (it !is UpdateCallLinkResult.Success) {
|
||||
Log.w(TAG, "Failed to update call link name")
|
||||
toastFailure()
|
||||
}
|
||||
}, onError = this::handleError)
|
||||
}
|
||||
|
||||
private fun setApproveAllMembers(approveAllMembers: Boolean) {
|
||||
lifecycleDisposable += viewModel.setApproveAllMembers(approveAllMembers).subscribeBy {
|
||||
}
|
||||
lifecycleDisposable += viewModel.setApproveAllMembers(approveAllMembers).subscribeBy(onSuccess = {
|
||||
if (it !is UpdateCallLinkResult.Success) {
|
||||
Log.w(TAG, "Failed to update call link restrictions")
|
||||
toastFailure()
|
||||
}
|
||||
}, onError = this::handleError)
|
||||
}
|
||||
|
||||
private fun toggleApproveAllMembers() {
|
||||
lifecycleDisposable += viewModel.toggleApproveAllMembers().subscribeBy {
|
||||
}
|
||||
lifecycleDisposable += viewModel.toggleApproveAllMembers().subscribeBy(onSuccess = {
|
||||
if (it !is UpdateCallLinkResult.Success) {
|
||||
Log.w(TAG, "Failed to update call link restrictions")
|
||||
toastFailure()
|
||||
}
|
||||
}, onError = this::handleError)
|
||||
}
|
||||
|
||||
private fun onAddACallNameClicked() {
|
||||
|
@ -172,59 +186,98 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
|
|||
}
|
||||
|
||||
private fun onJoinClicked() {
|
||||
lifecycleDisposable += viewModel.commitCallLink().subscribeBy {
|
||||
}
|
||||
lifecycleDisposable += viewModel.commitCallLink().subscribeBy(onSuccess = {
|
||||
when (it) {
|
||||
is EnsureCallLinkCreatedResult.Success -> {
|
||||
CommunicationActions.startVideoCall(requireActivity(), it.recipient)
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
is EnsureCallLinkCreatedResult.Failure -> handleCreateCallLinkFailure(it.failure)
|
||||
}
|
||||
}, onError = this::handleError)
|
||||
}
|
||||
|
||||
private fun onDoneClicked() {
|
||||
lifecycleDisposable += viewModel.commitCallLink().subscribeBy {
|
||||
lifecycleDisposable += viewModel.commitCallLink().subscribeBy(onSuccess = {
|
||||
when (it) {
|
||||
is EnsureCallLinkCreatedResult.Success -> dismissAllowingStateLoss()
|
||||
is EnsureCallLinkCreatedResult.Failure -> handleCreateCallLinkFailure(it.failure)
|
||||
}
|
||||
}, onError = this::handleError)
|
||||
}
|
||||
|
||||
private fun onShareViaSignalClicked() {
|
||||
lifecycleDisposable += viewModel.commitCallLink().subscribeBy(onSuccess = {
|
||||
when (it) {
|
||||
is EnsureCallLinkCreatedResult.Success -> {
|
||||
MultiselectForwardFragment.showFullScreen(
|
||||
childFragmentManager,
|
||||
MultiselectForwardFragmentArgs(
|
||||
canSendToNonPush = false,
|
||||
multiShareArgs = listOf(
|
||||
MultiShareArgs.Builder()
|
||||
.withDraftText(CallLinks.url(viewModel.linkKeyBytes))
|
||||
.build()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is EnsureCallLinkCreatedResult.Failure -> handleCreateCallLinkFailure(it.failure)
|
||||
}
|
||||
}, onError = this::handleError)
|
||||
}
|
||||
|
||||
private fun onCopyLinkClicked() {
|
||||
lifecycleDisposable += viewModel.commitCallLink().subscribeBy(onSuccess = {
|
||||
when (it) {
|
||||
is EnsureCallLinkCreatedResult.Success -> {
|
||||
Util.copyToClipboard(requireContext(), CallLinks.url(viewModel.linkKeyBytes))
|
||||
Toast.makeText(requireContext(), R.string.CreateCallLinkBottomSheetDialogFragment__copied_to_clipboard, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
is EnsureCallLinkCreatedResult.Failure -> handleCreateCallLinkFailure(it.failure)
|
||||
}
|
||||
}, onError = this::handleError)
|
||||
}
|
||||
|
||||
private fun onShareLinkClicked() {
|
||||
lifecycleDisposable += viewModel.commitCallLink().subscribeBy {
|
||||
when (it) {
|
||||
is EnsureCallLinkCreatedResult.Success -> {
|
||||
val mimeType = Intent.normalizeMimeType("text/plain")
|
||||
val shareIntent = ShareCompat.IntentBuilder(requireContext())
|
||||
.setText(CallLinks.url(viewModel.linkKeyBytes))
|
||||
.setType(mimeType)
|
||||
.createChooserIntent()
|
||||
|
||||
try {
|
||||
startActivity(shareIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(requireContext(), R.string.CreateCallLinkBottomSheetDialogFragment__failed_to_open_share_sheet, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
is EnsureCallLinkCreatedResult.Failure -> {
|
||||
Log.w(TAG, "Failed to create link: $it")
|
||||
toastFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCreateCallLinkFailure(failure: CreateCallLinkResult.Failure) {
|
||||
Log.w(TAG, "Failed to create call link: $failure")
|
||||
toastFailure()
|
||||
}
|
||||
|
||||
private fun onShareViaSignalClicked() {
|
||||
lifecycleDisposable += viewModel.commitCallLink().subscribeBy {
|
||||
MultiselectForwardFragment.showFullScreen(
|
||||
childFragmentManager,
|
||||
MultiselectForwardFragmentArgs(
|
||||
canSendToNonPush = false,
|
||||
multiShareArgs = listOf(
|
||||
MultiShareArgs.Builder()
|
||||
.withDraftText(CallLinks.url(viewModel.linkKeyBytes))
|
||||
.build()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
private fun handleError(throwable: Throwable) {
|
||||
Log.w(TAG, "Failed to create call link.", throwable)
|
||||
toastFailure()
|
||||
}
|
||||
|
||||
private fun onCopyLinkClicked() {
|
||||
lifecycleDisposable += viewModel.commitCallLink().subscribeBy {
|
||||
Util.copyToClipboard(requireContext(), CallLinks.url(viewModel.linkKeyBytes))
|
||||
Toast.makeText(requireContext(), R.string.CreateCallLinkBottomSheetDialogFragment__copied_to_clipboard, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onShareLinkClicked() {
|
||||
lifecycleDisposable += viewModel.commitCallLink().subscribeBy {
|
||||
val mimeType = Intent.normalizeMimeType("text/plain")
|
||||
val shareIntent = ShareCompat.IntentBuilder(requireContext())
|
||||
.setText(CallLinks.url(viewModel.linkKeyBytes))
|
||||
.setType(mimeType)
|
||||
.createChooserIntent()
|
||||
|
||||
try {
|
||||
startActivity(shareIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(requireContext(), R.string.CreateCallLinkBottomSheetDialogFragment__failed_to_open_share_sheet, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
private fun toastFailure() {
|
||||
Toast.makeText(requireContext(), R.string.CallLinkDetailsFragment__couldnt_save_changes, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
|||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CreateCallLinkResult
|
||||
|
@ -24,13 +25,13 @@ class CreateCallLinkRepository(
|
|||
private val callLinkManager: SignalCallLinkManager = ApplicationDependencies.getSignalCallManager().callLinkManager
|
||||
) {
|
||||
fun ensureCallLinkCreated(credentials: CallLinkCredentials, avatarColor: AvatarColor): Single<EnsureCallLinkCreatedResult> {
|
||||
val doesCallLinkExistInLocalDatabase = Single.fromCallable {
|
||||
SignalDatabase.callLinks.callLinkExists(credentials.roomId)
|
||||
val callLinkRecipientId = Single.fromCallable {
|
||||
SignalDatabase.recipients.getByCallLinkRoomId(credentials.roomId)
|
||||
}
|
||||
|
||||
return doesCallLinkExistInLocalDatabase.flatMap { exists ->
|
||||
if (exists) {
|
||||
Single.just(EnsureCallLinkCreatedResult.Success)
|
||||
return callLinkRecipientId.flatMap { recipientId ->
|
||||
if (recipientId.isPresent) {
|
||||
Single.just(EnsureCallLinkCreatedResult.Success(Recipient.resolved(recipientId.get())))
|
||||
} else {
|
||||
callLinkManager.createCallLink(credentials).map {
|
||||
when (it) {
|
||||
|
@ -45,7 +46,11 @@ class CreateCallLinkRepository(
|
|||
)
|
||||
)
|
||||
|
||||
EnsureCallLinkCreatedResult.Success
|
||||
EnsureCallLinkCreatedResult.Success(
|
||||
Recipient.resolved(
|
||||
SignalDatabase.recipients.getByCallLinkRoomId(credentials.roomId).get()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is CreateCallLinkResult.Failure -> EnsureCallLinkCreatedResult.Failure(it)
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
|
||||
package org.thoughtcrime.securesms.calls.links.create
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CreateCallLinkResult
|
||||
|
||||
sealed interface EnsureCallLinkCreatedResult {
|
||||
object Success : EnsureCallLinkCreatedResult
|
||||
data class Success(val recipient: Recipient) : EnsureCallLinkCreatedResult
|
||||
data class Failure(val failure: CreateCallLinkResult.Failure) : EnsureCallLinkCreatedResult
|
||||
}
|
||||
|
|
|
@ -5,11 +5,24 @@
|
|||
|
||||
package org.thoughtcrime.securesms.calls.links.details
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.FragmentWrapperActivity
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
|
||||
class CallLinkDetailsActivity : FragmentWrapperActivity() {
|
||||
override fun getFragment(): Fragment = NavHostFragment.create(R.navigation.call_link_details)
|
||||
override fun getFragment(): Fragment = NavHostFragment.create(R.navigation.call_link_details, intent.extras!!.getBundle(BUNDLE))
|
||||
|
||||
companion object {
|
||||
|
||||
private const val BUNDLE = "bundle"
|
||||
|
||||
fun createIntent(context: Context, callLinkRoomId: CallLinkRoomId): Intent {
|
||||
return Intent(context, CallLinkDetailsActivity::class.java)
|
||||
.putExtra(BUNDLE, CallLinkDetailsFragmentArgs.Builder(callLinkRoomId).build().toBundle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,15 +24,20 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.ui.Dialogs
|
||||
import org.signal.core.ui.Dividers
|
||||
import org.signal.core.ui.Rows
|
||||
import org.signal.core.ui.Scaffolds
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
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.links.CallLinks
|
||||
|
@ -44,6 +49,8 @@ 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.SignalCallLinkState
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
|
@ -52,7 +59,14 @@ import java.time.Instant
|
|||
*/
|
||||
class CallLinkDetailsFragment : ComposeFragment(), CallLinkDetailsCallback {
|
||||
|
||||
private val viewModel: CallLinkDetailsViewModel by viewModels()
|
||||
companion object {
|
||||
private val TAG = Log.tag(CallLinkDetailsFragment::class.java)
|
||||
}
|
||||
|
||||
private val args: CallLinkDetailsFragmentArgs by navArgs()
|
||||
private val viewModel: CallLinkDetailsViewModel by viewModels(factoryProducer = {
|
||||
CallLinkDetailsViewModel.Factory(args.roomId)
|
||||
})
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
@ -75,11 +89,14 @@ class CallLinkDetailsFragment : ComposeFragment(), CallLinkDetailsCallback {
|
|||
}
|
||||
|
||||
override fun onNavigationClicked() {
|
||||
findNavController().popBackStack()
|
||||
ActivityCompat.finishAfterTransition(requireActivity())
|
||||
}
|
||||
|
||||
override fun onJoinClicked() {
|
||||
// TODO("Not yet implemented")
|
||||
val recipientSnapshot = viewModel.recipientSnapshot
|
||||
if (recipientSnapshot != null) {
|
||||
CommunicationActions.startVideoCall(this, recipientSnapshot)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEditNameClicked() {
|
||||
|
@ -104,19 +121,54 @@ class CallLinkDetailsFragment : ComposeFragment(), CallLinkDetailsCallback {
|
|||
}
|
||||
|
||||
override fun onDeleteClicked() {
|
||||
lifecycleDisposable += viewModel.revoke().subscribeBy {
|
||||
}
|
||||
viewModel.setDisplayRevocationDialog(true)
|
||||
}
|
||||
|
||||
override fun onDeleteConfirmed() {
|
||||
viewModel.setDisplayRevocationDialog(false)
|
||||
lifecycleDisposable += viewModel.revoke().observeOn(AndroidSchedulers.mainThread()).subscribeBy(onSuccess = {
|
||||
when (it) {
|
||||
is UpdateCallLinkResult.Success -> ActivityCompat.finishAfterTransition(requireActivity())
|
||||
else -> {
|
||||
Log.w(TAG, "Failed to revoke. $it")
|
||||
toastFailure()
|
||||
}
|
||||
}
|
||||
}, onError = handleError("onDeleteClicked"))
|
||||
}
|
||||
|
||||
override fun onDeleteCanceled() {
|
||||
viewModel.setDisplayRevocationDialog(false)
|
||||
}
|
||||
|
||||
override fun onApproveAllMembersChanged(checked: Boolean) {
|
||||
lifecycleDisposable += viewModel.setApproveAllMembers(checked).subscribeBy {
|
||||
}
|
||||
lifecycleDisposable += viewModel.setApproveAllMembers(checked).observeOn(AndroidSchedulers.mainThread()).subscribeBy(onSuccess = {
|
||||
if (it !is UpdateCallLinkResult.Success) {
|
||||
Log.w(TAG, "Failed to change restrictions. $it")
|
||||
toastFailure()
|
||||
}
|
||||
}, onError = handleError("onApproveAllMembersChanged"))
|
||||
}
|
||||
|
||||
private fun setName(name: String) {
|
||||
lifecycleDisposable += viewModel.setName(name).subscribeBy {
|
||||
lifecycleDisposable += viewModel.setName(name).observeOn(AndroidSchedulers.mainThread()).subscribeBy(onSuccess = {
|
||||
if (it !is UpdateCallLinkResult.Success) {
|
||||
Log.w(TAG, "Failed to set name. $it")
|
||||
toastFailure()
|
||||
}
|
||||
}, onError = handleError("setName"))
|
||||
}
|
||||
|
||||
private fun handleError(method: String): (throwable: Throwable) -> Unit {
|
||||
return {
|
||||
Log.w(TAG, "Failure during $method", it)
|
||||
toastFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private fun toastFailure() {
|
||||
Toast.makeText(requireContext(), R.string.CallLinkDetailsFragment__couldnt_save_changes, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
private interface CallLinkDetailsCallback {
|
||||
|
@ -125,6 +177,8 @@ private interface CallLinkDetailsCallback {
|
|||
fun onEditNameClicked()
|
||||
fun onShareClicked()
|
||||
fun onDeleteClicked()
|
||||
fun onDeleteConfirmed()
|
||||
fun onDeleteCanceled()
|
||||
fun onApproveAllMembersChanged(checked: Boolean)
|
||||
}
|
||||
|
||||
|
@ -154,9 +208,12 @@ private fun CallLinkDetailsPreview() {
|
|||
SignalTheme(false) {
|
||||
CallLinkDetails(
|
||||
CallLinkDetailsState(
|
||||
false,
|
||||
callLink
|
||||
),
|
||||
object : CallLinkDetailsCallback {
|
||||
override fun onDeleteConfirmed() = Unit
|
||||
override fun onDeleteCanceled() = Unit
|
||||
override fun onNavigationClicked() = Unit
|
||||
override fun onJoinClicked() = Unit
|
||||
override fun onEditNameClicked() = Unit
|
||||
|
@ -215,5 +272,16 @@ private fun CallLinkDetails(
|
|||
modifier = Modifier.clickable(onClick = callback::onDeleteClicked)
|
||||
)
|
||||
}
|
||||
|
||||
if (state.displayRevocationDialog) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.CallLinkDetailsFragment__delete_link),
|
||||
body = stringResource(id = R.string.CallLinkDetailsFragment__this_link_will_no_longer_work),
|
||||
confirm = stringResource(id = R.string.delete),
|
||||
dismiss = stringResource(id = android.R.string.cancel),
|
||||
onConfirm = callback::onDeleteConfirmed,
|
||||
onDismiss = callback::onDeleteCanceled
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,12 +6,16 @@
|
|||
package org.thoughtcrime.securesms.calls.links.details
|
||||
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.orNull
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
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.links.ReadCallLinkResult
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager
|
||||
|
@ -23,11 +27,18 @@ class CallLinkDetailsRepository(
|
|||
return Maybe.fromCallable<CallLinkTable.CallLink> { SignalDatabase.callLinks.getCallLinkByRoomId(callLinkRoomId) }
|
||||
.flatMapSingle { callLinkManager.readCallLink(it.credentials!!) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribeBy {
|
||||
when (it) {
|
||||
is ReadCallLinkResult.Success -> SignalDatabase.callLinks.updateCallLinkState(callLinkRoomId, it.callLinkState)
|
||||
.subscribeBy { result ->
|
||||
when (result) {
|
||||
is ReadCallLinkResult.Success -> SignalDatabase.callLinks.updateCallLinkState(callLinkRoomId, result.callLinkState)
|
||||
is ReadCallLinkResult.Failure -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun watchCallLinkRecipient(callLinkRoomId: CallLinkRoomId): Observable<Recipient> {
|
||||
return Maybe.fromCallable<RecipientId> { SignalDatabase.recipients.getByCallLinkRoomId(callLinkRoomId).orNull() }
|
||||
.flatMapObservable { Recipient.observable(it) }
|
||||
.distinctUntilChanged { a, b -> a.hasSameContent(b) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,11 @@
|
|||
|
||||
package org.thoughtcrime.securesms.calls.links.details
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
|
||||
@Immutable
|
||||
data class CallLinkDetailsState(
|
||||
val displayRevocationDialog: Boolean = false,
|
||||
val callLink: CallLinkTable.CallLink? = null
|
||||
)
|
||||
|
|
|
@ -9,19 +9,22 @@ import androidx.compose.runtime.MutableState
|
|||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
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 io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import org.signal.ringrtc.CallLinkState
|
||||
import org.thoughtcrime.securesms.calls.links.CallLinks
|
||||
import org.thoughtcrime.securesms.calls.links.UpdateCallLinkRepository
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
|
||||
|
||||
class CallLinkDetailsViewModel(
|
||||
private val callLinkRoomId: CallLinkRoomId,
|
||||
private val repository: CallLinkDetailsRepository = CallLinkDetailsRepository(),
|
||||
callLinkRoomId: CallLinkRoomId,
|
||||
repository: CallLinkDetailsRepository = CallLinkDetailsRepository(),
|
||||
private val mutationRepository: UpdateCallLinkRepository = UpdateCallLinkRepository()
|
||||
) : ViewModel() {
|
||||
private val disposables = CompositeDisposable()
|
||||
|
@ -34,13 +37,19 @@ class CallLinkDetailsViewModel(
|
|||
val rootKeySnapshot: ByteArray
|
||||
get() = state.value.callLink?.credentials?.linkKeyBytes ?: error("Call link not loaded yet.")
|
||||
|
||||
private val recipientSubject = BehaviorSubject.create<Recipient>()
|
||||
val recipientSnapshot: Recipient?
|
||||
get() = recipientSubject.value
|
||||
|
||||
init {
|
||||
disposables += repository.refreshCallLinkState(callLinkRoomId)
|
||||
disposables += CallLinks.watchCallLink(callLinkRoomId).subscribeBy {
|
||||
_state.value = CallLinkDetailsState(
|
||||
callLink = it
|
||||
)
|
||||
_state.value = _state.value.copy(callLink = it)
|
||||
}
|
||||
|
||||
disposables += repository
|
||||
.watchCallLinkRecipient(callLinkRoomId)
|
||||
.subscribeBy(onNext = recipientSubject::onNext)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
|
@ -48,6 +57,10 @@ class CallLinkDetailsViewModel(
|
|||
disposables.dispose()
|
||||
}
|
||||
|
||||
fun setDisplayRevocationDialog(displayRevocationDialog: Boolean) {
|
||||
_state.value = _state.value.copy(displayRevocationDialog = displayRevocationDialog)
|
||||
}
|
||||
|
||||
fun setApproveAllMembers(approveAllMembers: Boolean): Single<UpdateCallLinkResult> {
|
||||
val credentials = _state.value.callLink?.credentials ?: error("User cannot change the name of this call.")
|
||||
return mutationRepository.setCallRestrictions(credentials, if (approveAllMembers) CallLinkState.Restrictions.ADMIN_APPROVAL else CallLinkState.Restrictions.NONE)
|
||||
|
@ -62,4 +75,10 @@ class CallLinkDetailsViewModel(
|
|||
val credentials = _state.value.callLink?.credentials ?: error("User cannot change the name of this call.")
|
||||
return mutationRepository.revokeCallLink(credentials)
|
||||
}
|
||||
|
||||
class Factory(private val callLinkRoomId: CallLinkRoomId) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(CallLinkDetailsViewModel(callLinkRoomId)) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,6 +62,14 @@ class CallLogAdapter(
|
|||
inflater = CallLogCreateCallLinkItemBinding::inflate
|
||||
)
|
||||
)
|
||||
|
||||
registerFactory(
|
||||
CallLinkModel::class.java,
|
||||
BindingFactory(
|
||||
creator = { CallLinkModelViewHolder(it, callbacks::onCallLinkClicked, callbacks::onCallLinkLongClicked, callbacks::onStartVideoCallClicked) },
|
||||
inflater = CallLogAdapterItemBinding::inflate
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun submitCallRows(
|
||||
|
@ -76,6 +84,7 @@ class CallLogAdapter(
|
|||
.map {
|
||||
when (it) {
|
||||
is CallLogRow.Call -> CallModel(it, selectionState, itemCount)
|
||||
is CallLogRow.CallLink -> CallLinkModel(it, selectionState, itemCount)
|
||||
is CallLogRow.ClearFilter -> ClearFilterModel()
|
||||
is CallLogRow.CreateCallLink -> CreateCallLinkModel()
|
||||
}
|
||||
|
@ -120,6 +129,44 @@ class CallLogAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
private class CallLinkModel(
|
||||
val callLink: CallLogRow.CallLink,
|
||||
val selectionState: CallLogSelectionState,
|
||||
val itemCount: Int
|
||||
) : MappingModel<CallLinkModel> {
|
||||
|
||||
companion object {
|
||||
const val PAYLOAD_SELECTION_STATE = "PAYLOAD_SELECTION_STATE"
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(newItem: CallLinkModel): Boolean {
|
||||
return callLink.record.roomId == newItem.callLink.record.roomId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: CallLinkModel): Boolean {
|
||||
return callLink == newItem.callLink &&
|
||||
isSelectionStateTheSame(newItem) &&
|
||||
isItemCountTheSame(newItem)
|
||||
}
|
||||
|
||||
override fun getChangePayload(newItem: CallLinkModel): Any? {
|
||||
return if (callLink == newItem.callLink && (!isSelectionStateTheSame(newItem) || !isItemCountTheSame(newItem))) {
|
||||
CallModel.PAYLOAD_SELECTION_STATE
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSelectionStateTheSame(newItem: CallLinkModel): Boolean {
|
||||
return selectionState.contains(callLink.id) == newItem.selectionState.contains(newItem.callLink.id) &&
|
||||
selectionState.isNotEmpty(itemCount) == newItem.selectionState.isNotEmpty(newItem.itemCount)
|
||||
}
|
||||
|
||||
private fun isItemCountTheSame(newItem: CallLinkModel): Boolean {
|
||||
return itemCount == newItem.itemCount
|
||||
}
|
||||
}
|
||||
|
||||
private class ClearFilterModel : MappingModel<ClearFilterModel> {
|
||||
override fun areItemsTheSame(newItem: ClearFilterModel): Boolean = true
|
||||
override fun areContentsTheSame(newItem: ClearFilterModel): Boolean = true
|
||||
|
@ -131,6 +178,54 @@ class CallLogAdapter(
|
|||
override fun areContentsTheSame(newItem: CreateCallLinkModel): Boolean = true
|
||||
}
|
||||
|
||||
private class CallLinkModelViewHolder(
|
||||
binding: CallLogAdapterItemBinding,
|
||||
private val onCallLinkClicked: (CallLogRow.CallLink) -> Unit,
|
||||
private val onCallLinkLongClicked: (View, CallLogRow.CallLink) -> Boolean,
|
||||
private val onStartVideoCallClicked: (Recipient) -> Unit
|
||||
) : BindingViewHolder<CallLinkModel, CallLogAdapterItemBinding>(binding) {
|
||||
override fun bind(model: CallLinkModel) {
|
||||
itemView.setOnClickListener {
|
||||
onCallLinkClicked(model.callLink)
|
||||
}
|
||||
|
||||
itemView.setOnLongClickListener {
|
||||
onCallLinkLongClicked(itemView, model.callLink)
|
||||
}
|
||||
|
||||
itemView.isSelected = model.selectionState.contains(model.callLink.id)
|
||||
binding.callSelected.isChecked = model.selectionState.contains(model.callLink.id)
|
||||
binding.callSelected.visible = model.selectionState.isNotEmpty(model.itemCount)
|
||||
|
||||
if (payload.contains(CallModel.PAYLOAD_SELECTION_STATE)) {
|
||||
return
|
||||
}
|
||||
|
||||
binding.callRecipientAvatar.setAvatar(model.callLink.recipient)
|
||||
|
||||
val callLinkName = model.callLink.record.state.name.takeIf { it.isNotEmpty() }
|
||||
?: context.getString(R.string.WebRtcCallView__signal_call)
|
||||
|
||||
binding.callRecipientName.text = SearchUtil.getHighlightedSpan(
|
||||
Locale.getDefault(),
|
||||
{ arrayOf(TextAppearanceSpan(context, R.style.Signal_Text_TitleSmall)) },
|
||||
callLinkName,
|
||||
model.callLink.searchQuery,
|
||||
SearchUtil.MATCH_ALL
|
||||
)
|
||||
|
||||
binding.callInfo.setRelativeDrawables(start = R.drawable.symbol_link_compact_16)
|
||||
binding.callInfo.setText(R.string.CallLogAdapter__call_link)
|
||||
|
||||
binding.callType.setImageResource(R.drawable.symbol_video_24)
|
||||
binding.callType.setOnClickListener {
|
||||
onStartVideoCallClicked(model.callLink.recipient)
|
||||
}
|
||||
binding.callType.visible = true
|
||||
binding.groupCallButton.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
private class CallModelViewHolder(
|
||||
binding: CallLogAdapterItemBinding,
|
||||
private val onCallClicked: (CallLogRow.Call) -> Unit,
|
||||
|
@ -235,6 +330,7 @@ class CallLogAdapter(
|
|||
binding.callType.visible = true
|
||||
binding.groupCallButton.visible = false
|
||||
}
|
||||
|
||||
CallLogRow.GroupCallState.ACTIVE, CallLogRow.GroupCallState.LOCAL_USER_JOINED -> {
|
||||
binding.callType.visible = false
|
||||
binding.groupCallButton.visible = true
|
||||
|
@ -265,6 +361,7 @@ class CallLogAdapter(
|
|||
call.direction == CallTable.Direction.OUTGOING -> R.drawable.symbol_arrow_upright_compact_16
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
|
||||
else -> error("Unexpected type ${call.type}")
|
||||
}
|
||||
}
|
||||
|
@ -285,6 +382,7 @@ class CallLogAdapter(
|
|||
call.direction == CallTable.Direction.OUTGOING -> R.string.CallLogAdapter__outgoing
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
|
||||
else -> error("Unexpected type ${call.messageType}")
|
||||
}
|
||||
}
|
||||
|
@ -324,11 +422,21 @@ class CallLogAdapter(
|
|||
*/
|
||||
fun onCallClicked(callLogRow: CallLogRow.Call)
|
||||
|
||||
/**
|
||||
* Invoked when a call link row is clicked
|
||||
*/
|
||||
fun onCallLinkClicked(callLogRow: CallLogRow.CallLink)
|
||||
|
||||
/**
|
||||
* Invoked when a call row is long-clicked
|
||||
*/
|
||||
fun onCallLongClicked(itemView: View, callLogRow: CallLogRow.Call): Boolean
|
||||
|
||||
/**
|
||||
* Invoked when a call link row is long-clicked
|
||||
*/
|
||||
fun onCallLinkLongClicked(itemView: View, callLinkLogRow: CallLogRow.CallLink): Boolean
|
||||
|
||||
/**
|
||||
* Invoked when the clear filter button is pressed
|
||||
*/
|
||||
|
|
|
@ -5,11 +5,13 @@ import android.view.ViewGroup
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsActivity
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.database.CallTable
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
|
||||
/**
|
||||
|
@ -30,23 +32,42 @@ class CallLogContextMenu(
|
|||
}
|
||||
.show(
|
||||
listOfNotNull(
|
||||
getVideoCallActionItem(call),
|
||||
getVideoCallActionItem(call.peer),
|
||||
getAudioCallActionItem(call),
|
||||
getGoToChatActionItem(call),
|
||||
getInfoActionItem(call),
|
||||
getInfoActionItem(call.peer, (call.id as CallLogRow.Id.Call).children.toLongArray()),
|
||||
getSelectActionItem(call),
|
||||
getDeleteActionItem(call)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getVideoCallActionItem(call: CallLogRow.Call): ActionItem {
|
||||
fun show(recyclerView: RecyclerView, anchor: View, callLink: CallLogRow.CallLink) {
|
||||
recyclerView.suppressLayout(true)
|
||||
anchor.isSelected = true
|
||||
SignalContextMenu.Builder(anchor, anchor.parent as ViewGroup)
|
||||
.preferredVerticalPosition(SignalContextMenu.VerticalPosition.BELOW)
|
||||
.onDismiss {
|
||||
anchor.isSelected = false
|
||||
recyclerView.suppressLayout(false)
|
||||
}
|
||||
.show(
|
||||
listOfNotNull(
|
||||
getVideoCallActionItem(callLink.recipient),
|
||||
getInfoActionItem(callLink.recipient, longArrayOf()),
|
||||
getSelectActionItem(callLink),
|
||||
getDeleteActionItem(callLink)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getVideoCallActionItem(peer: Recipient): ActionItem {
|
||||
// TODO [alex] -- Need group calling disposition to make this correct
|
||||
return ActionItem(
|
||||
iconRes = R.drawable.symbol_video_24,
|
||||
title = fragment.getString(R.string.CallContextMenu__video_call)
|
||||
) {
|
||||
CommunicationActions.startVideoCall(fragment, call.peer)
|
||||
CommunicationActions.startVideoCall(fragment, peer)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,20 +96,20 @@ class CallLogContextMenu(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getInfoActionItem(call: CallLogRow.Call): ActionItem {
|
||||
private fun getInfoActionItem(peer: Recipient, messageIds: LongArray): ActionItem {
|
||||
return ActionItem(
|
||||
iconRes = R.drawable.symbol_info_24,
|
||||
title = fragment.getString(R.string.CallContextMenu__info)
|
||||
) {
|
||||
val intent = when {
|
||||
call.peer.isCallLink -> throw NotImplementedError("Launch CallLinkDetailsActivity")
|
||||
else -> ConversationSettingsActivity.forCall(fragment.requireContext(), call.peer, longArrayOf(call.record.messageId!!))
|
||||
peer.isCallLink -> CallLinkDetailsActivity.createIntent(fragment.requireContext(), peer.requireCallLinkRoomId())
|
||||
else -> ConversationSettingsActivity.forCall(fragment.requireContext(), peer, messageIds)
|
||||
}
|
||||
fragment.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSelectActionItem(call: CallLogRow.Call): ActionItem {
|
||||
private fun getSelectActionItem(call: CallLogRow): ActionItem {
|
||||
return ActionItem(
|
||||
iconRes = R.drawable.symbol_check_circle_24,
|
||||
title = fragment.getString(R.string.CallContextMenu__select)
|
||||
|
@ -97,8 +118,8 @@ class CallLogContextMenu(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getDeleteActionItem(call: CallLogRow.Call): ActionItem? {
|
||||
if (call.record.event == CallTable.Event.ONGOING) {
|
||||
private fun getDeleteActionItem(call: CallLogRow): ActionItem? {
|
||||
if (call is CallLogRow.Call && call.record.event == CallTable.Event.ONGOING) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -111,7 +132,7 @@ class CallLogContextMenu(
|
|||
}
|
||||
|
||||
interface Callbacks {
|
||||
fun startSelection(call: CallLogRow.Call)
|
||||
fun deleteCall(call: CallLogRow.Call)
|
||||
fun startSelection(call: CallLogRow)
|
||||
fun deleteCall(call: CallLogRow)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
|
|||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsActivity
|
||||
import org.thoughtcrime.securesms.calls.new.NewCallActivity
|
||||
import org.thoughtcrime.securesms.components.Material3SearchToolbar
|
||||
import org.thoughtcrime.securesms.components.ScrollToPositionDelegate
|
||||
|
@ -319,7 +320,15 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
|||
)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
throw NotImplementedError("On call link event clicked.")
|
||||
startActivity(CallLinkDetailsActivity.createIntent(requireContext(), callLogRow.peer.requireCallLinkRoomId()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCallLinkClicked(callLogRow: CallLogRow.CallLink) {
|
||||
if (viewModel.selectionStateSnapshot.isNotEmpty(binding.recycler.adapter!!.itemCount)) {
|
||||
viewModel.toggleSelected(callLogRow.id)
|
||||
} else {
|
||||
startActivity(CallLinkDetailsActivity.createIntent(requireContext(), callLogRow.record.roomId))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -328,6 +337,11 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
|||
return true
|
||||
}
|
||||
|
||||
override fun onCallLinkLongClicked(itemView: View, callLinkLogRow: CallLogRow.CallLink): Boolean {
|
||||
callLogContextMenu.show(binding.recycler, itemView, callLinkLogRow)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onClearFilterClicked() {
|
||||
binding.pullView.toggle()
|
||||
binding.recyclerCoordinatorAppBar.setExpanded(false, true)
|
||||
|
@ -341,12 +355,12 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
|||
CommunicationActions.startVideoCall(this, recipient)
|
||||
}
|
||||
|
||||
override fun startSelection(call: CallLogRow.Call) {
|
||||
override fun startSelection(call: CallLogRow) {
|
||||
callLogActionMode.start()
|
||||
viewModel.toggleSelected(call.id)
|
||||
}
|
||||
|
||||
override fun deleteCall(call: CallLogRow.Call) {
|
||||
override fun deleteCall(call: CallLogRow) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(resources.getQuantityString(R.plurals.CallLogFragment__delete_d_calls, 1, 1))
|
||||
.setPositiveButton(R.string.CallLogFragment__delete_for_me) { _, _ ->
|
||||
|
|
|
@ -12,28 +12,62 @@ class CallLogPagedDataSource(
|
|||
private val hasFilter = filter == CallLogFilter.MISSED
|
||||
private val hasCallLinkRow = FeatureFlags.adHocCalling() && filter == CallLogFilter.ALL && query.isNullOrEmpty()
|
||||
|
||||
private var callsCount = 0
|
||||
private var callEventsCount = 0
|
||||
private var callLinksCount = 0
|
||||
|
||||
override fun size(): Int {
|
||||
callsCount = repository.getCallsCount(query, filter)
|
||||
return callsCount + hasFilter.toInt() + hasCallLinkRow.toInt()
|
||||
callEventsCount = repository.getCallsCount(query, filter)
|
||||
callLinksCount = repository.getCallLinksCount(query, filter)
|
||||
return callEventsCount + callLinksCount + hasFilter.toInt() + hasCallLinkRow.toInt()
|
||||
}
|
||||
|
||||
override fun load(start: Int, length: Int, totalSize: Int, cancellationSignal: PagedDataSource.CancellationSignal): MutableList<CallLogRow> {
|
||||
val calls = mutableListOf<CallLogRow>()
|
||||
val callLimit = length - hasCallLinkRow.toInt()
|
||||
|
||||
if (start == 0 && length >= 1 && hasCallLinkRow) {
|
||||
calls.add(CallLogRow.CreateCallLink)
|
||||
val callLogRows = mutableListOf<CallLogRow>()
|
||||
if (length <= 0) {
|
||||
return callLogRows
|
||||
}
|
||||
|
||||
calls.addAll(repository.getCalls(query, filter, start, callLimit).toMutableList())
|
||||
val callLinkStart = if (hasCallLinkRow) 1 else 0
|
||||
val callEventStart = callLinkStart + callLinksCount
|
||||
val clearFilterStart = callEventStart + callEventsCount
|
||||
|
||||
if (calls.size < length && hasFilter) {
|
||||
calls.add(CallLogRow.ClearFilter)
|
||||
var remaining = length
|
||||
if (start < callLinkStart) {
|
||||
callLogRows.add(CallLogRow.CreateCallLink)
|
||||
remaining -= 1
|
||||
}
|
||||
|
||||
return calls
|
||||
if (start < callEventStart && remaining > 0) {
|
||||
val callLinks = repository.getCallLinks(
|
||||
query,
|
||||
filter,
|
||||
start,
|
||||
remaining
|
||||
)
|
||||
|
||||
callLogRows.addAll(callLinks)
|
||||
|
||||
remaining -= callLinks.size
|
||||
}
|
||||
|
||||
if (start < clearFilterStart && remaining > 0) {
|
||||
val callEvents = repository.getCalls(
|
||||
query,
|
||||
filter,
|
||||
start - callLinksCount,
|
||||
remaining
|
||||
)
|
||||
|
||||
callLogRows.addAll(callEvents)
|
||||
|
||||
remaining -= callEvents.size
|
||||
}
|
||||
|
||||
if (start <= clearFilterStart && remaining > 0) {
|
||||
callLogRows.add(CallLogRow.ClearFilter)
|
||||
}
|
||||
|
||||
return callLogRows
|
||||
}
|
||||
|
||||
override fun getKey(data: CallLogRow): CallLogRow.Id = data.id
|
||||
|
@ -47,5 +81,7 @@ class CallLogPagedDataSource(
|
|||
interface CallRepository {
|
||||
fun getCallsCount(query: String?, filter: CallLogFilter): Int
|
||||
fun getCalls(query: String?, filter: CallLogFilter, start: Int, length: Int): List<CallLogRow>
|
||||
fun getCallLinksCount(query: String?, filter: CallLogFilter): Int
|
||||
fun getCallLinks(query: String?, filter: CallLogFilter, start: Int, length: Int): List<CallLogRow>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,20 @@ class CallLogRepository : CallLogPagedDataSource.CallRepository {
|
|||
return SignalDatabase.calls.getCalls(start, length, query, filter)
|
||||
}
|
||||
|
||||
override fun getCallLinksCount(query: String?, filter: CallLogFilter): Int {
|
||||
return when (filter) {
|
||||
CallLogFilter.MISSED -> 0
|
||||
CallLogFilter.ALL -> SignalDatabase.callLinks.getCallLinksCount(query)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCallLinks(query: String?, filter: CallLogFilter, start: Int, length: Int): List<CallLogRow> {
|
||||
return when (filter) {
|
||||
CallLogFilter.MISSED -> emptyList()
|
||||
CallLogFilter.ALL -> SignalDatabase.callLinks.getCallLinks(query, start, length)
|
||||
}
|
||||
}
|
||||
|
||||
fun markAllCallEventsRead() {
|
||||
SignalExecutors.BOUNDED_IO.execute {
|
||||
SignalDatabase.messages.markAllCallEventsRead()
|
||||
|
|
|
@ -5,9 +5,11 @@
|
|||
|
||||
package org.thoughtcrime.securesms.calls.log
|
||||
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.database.CallTable
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
|
||||
/**
|
||||
* A row to be displayed in the call log
|
||||
|
@ -16,6 +18,16 @@ sealed class CallLogRow {
|
|||
|
||||
abstract val id: Id
|
||||
|
||||
/**
|
||||
* A call link with no "active" events.
|
||||
*/
|
||||
data class CallLink(
|
||||
val record: CallLinkTable.CallLink,
|
||||
val recipient: Recipient,
|
||||
val searchQuery: String?,
|
||||
override val id: Id = Id.CallLink(record.roomId)
|
||||
) : CallLogRow()
|
||||
|
||||
/**
|
||||
* An incoming, outgoing, or missed call.
|
||||
*/
|
||||
|
@ -42,6 +54,7 @@ sealed class CallLogRow {
|
|||
|
||||
sealed class Id {
|
||||
data class Call(val children: Set<Long>) : Id()
|
||||
data class CallLink(val roomId: CallLinkRoomId) : Id()
|
||||
object ClearFilter : Id()
|
||||
object CreateCallLink : Id()
|
||||
}
|
||||
|
|
|
@ -96,7 +96,7 @@ class CallLogViewModel(
|
|||
}
|
||||
|
||||
@MainThread
|
||||
fun stageCallDeletion(call: CallLogRow.Call) {
|
||||
fun stageCallDeletion(call: CallLogRow) {
|
||||
callLogStore.state.stagedDeletion?.commit()
|
||||
callLogStore.update {
|
||||
it.copy(
|
||||
|
|
|
@ -5,8 +5,10 @@ import android.content.Context
|
|||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import org.signal.core.util.Serializer
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.insertInto
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.readToSingleInt
|
||||
import org.signal.core.util.readToSingleObject
|
||||
import org.signal.core.util.requireBlob
|
||||
|
@ -20,8 +22,10 @@ import org.signal.core.util.select
|
|||
import org.signal.core.util.update
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.ringrtc.CallLinkState.Restrictions
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogRow
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
|
@ -88,7 +92,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
|||
callLink: CallLink
|
||||
) {
|
||||
writableDatabase.withinTransaction { db ->
|
||||
val recipientId = SignalDatabase.recipients.getOrInsertFromCallLinkRoomId(callLink.roomId)
|
||||
val recipientId = SignalDatabase.recipients.getOrInsertFromCallLinkRoomId(callLink.roomId, callLink.avatarColor)
|
||||
|
||||
db
|
||||
.insertInto(TABLE_NAME)
|
||||
|
@ -97,6 +101,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
|||
}
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().notifyCallLinkObservers(callLink.roomId)
|
||||
ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
|
||||
}
|
||||
|
||||
fun updateCallLinkCredentials(
|
||||
|
@ -115,6 +120,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
|||
.run()
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().notifyCallLinkObservers(roomId)
|
||||
ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
|
||||
}
|
||||
|
||||
fun updateCallLinkState(
|
||||
|
@ -128,6 +134,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
|||
.run()
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().notifyCallLinkObservers(roomId)
|
||||
ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
|
||||
}
|
||||
|
||||
fun callLinkExists(
|
||||
|
@ -171,6 +178,59 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
|||
}
|
||||
}
|
||||
|
||||
fun getCallLinksCount(query: String?): Int {
|
||||
return queryCallLinks(query, -1, -1, true).readToSingleInt(0)
|
||||
}
|
||||
|
||||
fun getCallLinks(query: String?, offset: Int, limit: Int): List<CallLogRow.CallLink> {
|
||||
return queryCallLinks(query, offset, limit, false).readToList {
|
||||
val callLink = CallLinkDeserializer.deserialize(it)
|
||||
CallLogRow.CallLink(callLink, Recipient.resolved(callLink.recipientId), query)
|
||||
}
|
||||
}
|
||||
|
||||
private fun queryCallLinks(query: String?, offset: Int, limit: Int, asCount: Boolean): Cursor {
|
||||
//language=sql
|
||||
val noCallEvent = """
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM ${CallTable.TABLE_NAME}
|
||||
WHERE ${CallTable.PEER} = $TABLE_NAME.$RECIPIENT_ID
|
||||
AND ${CallTable.TYPE} = ${CallTable.Type.serialize(CallTable.Type.AD_HOC_CALL)}
|
||||
AND ${CallTable.EVENT} != ${CallTable.Event.serialize(CallTable.Event.DELETE)}
|
||||
)
|
||||
""".trimIndent()
|
||||
|
||||
val searchFilter = if (!query.isNullOrEmpty()) {
|
||||
SqlUtil.buildQuery("AND $NAME GLOB ?", SqlUtil.buildCaseInsensitiveGlobPattern(query))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val limitOffset = if (limit >= 0 && offset >= 0) {
|
||||
//language=sql
|
||||
"LIMIT $limit OFFSET $offset"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
val projection = if (asCount) {
|
||||
"COUNT(*)"
|
||||
} else {
|
||||
"*"
|
||||
}
|
||||
|
||||
//language=sql
|
||||
val statement = """
|
||||
SELECT $projection
|
||||
FROM $TABLE_NAME
|
||||
WHERE $noCallEvent AND NOT $REVOKED ${searchFilter?.where ?: ""}
|
||||
$limitOffset
|
||||
""".trimIndent()
|
||||
|
||||
return readableDatabase.query(statement, searchFilter?.whereArgs)
|
||||
}
|
||||
|
||||
private object CallLinkSerializer : Serializer<CallLink, ContentValues> {
|
||||
override fun serialize(data: CallLink): ContentValues {
|
||||
return contentValuesOf(
|
||||
|
|
|
@ -47,14 +47,14 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
|||
private val TAG = Log.tag(CallTable::class.java)
|
||||
private val TIME_WINDOW = TimeUnit.HOURS.toMillis(4)
|
||||
|
||||
private const val TABLE_NAME = "call"
|
||||
const val TABLE_NAME = "call"
|
||||
private const val ID = "_id"
|
||||
private const val CALL_ID = "call_id"
|
||||
private const val MESSAGE_ID = "message_id"
|
||||
private const val PEER = "peer"
|
||||
private const val TYPE = "type"
|
||||
const val PEER = "peer"
|
||||
const val TYPE = "type"
|
||||
private const val DIRECTION = "direction"
|
||||
private const val EVENT = "event"
|
||||
const val EVENT = "event"
|
||||
private const val TIMESTAMP = "timestamp"
|
||||
private const val RINGER = "ringer"
|
||||
private const val DELETION_TIMESTAMP = "deletion_timestamp"
|
||||
|
|
|
@ -319,7 +319,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
DISTRIBUTION_LIST_ID,
|
||||
NEEDS_PNI_SIGNATURE,
|
||||
HIDDEN,
|
||||
REPORTING_TOKEN
|
||||
REPORTING_TOKEN,
|
||||
CALL_LINK_ROOM_ID
|
||||
)
|
||||
|
||||
private val ID_PROJECTION = arrayOf(ID)
|
||||
|
@ -564,14 +565,15 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
).recipientId
|
||||
}
|
||||
|
||||
fun getOrInsertFromCallLinkRoomId(callLinkRoomId: CallLinkRoomId): RecipientId {
|
||||
fun getOrInsertFromCallLinkRoomId(callLinkRoomId: CallLinkRoomId, avatarColor: AvatarColor): RecipientId {
|
||||
return getOrInsertByColumn(
|
||||
CALL_LINK_ROOM_ID,
|
||||
callLinkRoomId.serialize(),
|
||||
contentValuesOf(
|
||||
GROUP_TYPE to GroupType.CALL_LINK.id,
|
||||
CALL_LINK_ROOM_ID to callLinkRoomId.serialize(),
|
||||
PROFILE_SHARING to 1
|
||||
PROFILE_SHARING to 1,
|
||||
AVATAR_COLOR to avatarColor.serialize()
|
||||
)
|
||||
).recipientId
|
||||
}
|
||||
|
@ -4172,7 +4174,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
hasGroupsInCommon = cursor.requireBoolean(GROUPS_IN_COMMON),
|
||||
badges = parseBadgeList(cursor.requireBlob(BADGES)),
|
||||
needsPniSignature = cursor.requireBoolean(NEEDS_PNI_SIGNATURE),
|
||||
isHidden = cursor.requireBoolean(HIDDEN)
|
||||
isHidden = cursor.requireBoolean(HIDDEN),
|
||||
callLinkRoomId = cursor.requireString(CALL_LINK_ROOM_ID)?.let { CallLinkRoomId.DatabaseSerializer.deserialize(it) }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.groups.GroupId
|
|||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
|
@ -79,7 +80,8 @@ data class RecipientRecord(
|
|||
val badges: List<Badge>,
|
||||
@get:JvmName("needsPniSignature")
|
||||
val needsPniSignature: Boolean,
|
||||
val isHidden: Boolean
|
||||
val isHidden: Boolean,
|
||||
val callLinkRoomId: CallLinkRoomId?
|
||||
) {
|
||||
|
||||
fun getDefaultSubscriptionId(): Optional<Int> {
|
||||
|
|
|
@ -13,6 +13,7 @@ import com.annimon.stream.Stream;
|
|||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable;
|
||||
import org.thoughtcrime.securesms.database.DistributionListTables;
|
||||
import org.thoughtcrime.securesms.database.GroupTable;
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord;
|
||||
|
@ -194,7 +195,9 @@ public final class LiveRecipient {
|
|||
details = getGroupRecipientDetails(record);
|
||||
} else if (record.getDistributionListId() != null) {
|
||||
details = getDistributionListRecipientDetails(record);
|
||||
} else {
|
||||
} else if (record.getCallLinkRoomId() != null) {
|
||||
details = getCallLinkRecipientDetails(record);
|
||||
}else {
|
||||
details = RecipientDetails.forIndividual(context, record);
|
||||
}
|
||||
|
||||
|
@ -237,6 +240,19 @@ public final class LiveRecipient {
|
|||
return RecipientDetails.forDistributionList(null, null, record);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull RecipientDetails getCallLinkRecipientDetails(@NonNull RecipientRecord record) {
|
||||
CallLinkTable.CallLink callLink = SignalDatabase.callLinks().getCallLinkByRoomId(Objects.requireNonNull(record.getCallLinkRoomId()));
|
||||
|
||||
if (callLink != null) {
|
||||
String name = callLink.getState().getName();
|
||||
|
||||
return RecipientDetails.forCallLink(name, record);
|
||||
}
|
||||
|
||||
return RecipientDetails.forCallLink(null, record);
|
||||
}
|
||||
|
||||
synchronized void set(@NonNull Recipient recipient) {
|
||||
this.recipient.set(recipient);
|
||||
this.liveData.postValue(recipient);
|
||||
|
|
|
@ -136,6 +136,7 @@ public class Recipient {
|
|||
private final List<Badge> badges;
|
||||
private final boolean isReleaseNotesRecipient;
|
||||
private final boolean needsPniSignature;
|
||||
private final CallLinkRoomId callLinkRoomId;
|
||||
|
||||
/**
|
||||
* Returns a {@link LiveRecipient}, which contains a {@link Recipient} that may or may not be
|
||||
|
@ -433,6 +434,7 @@ public class Recipient {
|
|||
this.isReleaseNotesRecipient = false;
|
||||
this.needsPniSignature = false;
|
||||
this.isActiveGroup = false;
|
||||
this.callLinkRoomId = null;
|
||||
}
|
||||
|
||||
public Recipient(@NonNull RecipientId id, @NonNull RecipientDetails details, boolean resolved) {
|
||||
|
@ -489,6 +491,7 @@ public class Recipient {
|
|||
this.isReleaseNotesRecipient = details.isReleaseChannel;
|
||||
this.needsPniSignature = details.needsPniSignature;
|
||||
this.isActiveGroup = details.isActiveGroup;
|
||||
this.callLinkRoomId = details.callLinkRoomId;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getId() {
|
||||
|
@ -541,6 +544,8 @@ public class Recipient {
|
|||
return Util.join(names, ", ");
|
||||
} else if (!resolving && isMyStory()) {
|
||||
return context.getString(R.string.Recipient_my_story);
|
||||
} else if (!resolving && Util.isEmpty(this.groupName) && isCallLink()){
|
||||
return context.getString(R.string.Recipient_signal_call);
|
||||
} else {
|
||||
return this.groupName;
|
||||
}
|
||||
|
@ -932,6 +937,7 @@ public class Recipient {
|
|||
if (isSelf) return fallbackPhotoProvider.getPhotoForLocalNumber();
|
||||
else if (isResolving()) return fallbackPhotoProvider.getPhotoForResolvingRecipient();
|
||||
else if (isDistributionList()) return fallbackPhotoProvider.getPhotoForDistributionList();
|
||||
else if (isCallLink()) return fallbackPhotoProvider.getPhotoForCallLink();
|
||||
else if (isGroupInternal()) return fallbackPhotoProvider.getPhotoForGroup();
|
||||
else if (isGroup()) return fallbackPhotoProvider.getPhotoForGroup();
|
||||
else if (!TextUtils.isEmpty(groupName)) return fallbackPhotoProvider.getPhotoForRecipientWithName(groupName, targetSize);
|
||||
|
@ -1209,11 +1215,11 @@ public class Recipient {
|
|||
}
|
||||
|
||||
public boolean isCallLink() {
|
||||
return false;
|
||||
return callLinkRoomId != null;
|
||||
}
|
||||
|
||||
public @NonNull CallLinkRoomId requireCallLinkRoomId() {
|
||||
throw new UnsupportedOperationException();
|
||||
return Objects.requireNonNull(callLinkRoomId);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1348,7 +1354,8 @@ public class Recipient {
|
|||
Objects.equals(aboutEmoji, other.aboutEmoji) &&
|
||||
Objects.equals(extras, other.extras) &&
|
||||
hasGroupsInCommon == other.hasGroupsInCommon &&
|
||||
Objects.equals(badges, other.badges);
|
||||
Objects.equals(badges, other.badges) &&
|
||||
Objects.equals(callLinkRoomId, other.callLinkRoomId);
|
||||
}
|
||||
|
||||
private static boolean allContentsAreTheSame(@NonNull List<Recipient> a, @NonNull List<Recipient> b) {
|
||||
|
@ -1390,6 +1397,10 @@ public class Recipient {
|
|||
public @NonNull FallbackContactPhoto getPhotoForDistributionList() {
|
||||
return new ResourceContactPhoto(R.drawable.symbol_stories_24, R.drawable.symbol_stories_24, R.drawable.symbol_stories_24);
|
||||
}
|
||||
|
||||
public @NonNull FallbackContactPhoto getPhotoForCallLink() {
|
||||
return new ResourceContactPhoto(R.drawable.symbol_video_24, R.drawable.symbol_video_24, R.drawable.symbol_video_24);
|
||||
}
|
||||
}
|
||||
|
||||
private static class MissingAddressError extends AssertionError {
|
||||
|
|
|
@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.database.model.RecipientRecord;
|
|||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
|
||||
|
@ -86,6 +87,7 @@ public class RecipientDetails {
|
|||
final List<Badge> badges;
|
||||
final boolean isReleaseChannel;
|
||||
final boolean needsPniSignature;
|
||||
final CallLinkRoomId callLinkRoomId;
|
||||
|
||||
public RecipientDetails(@Nullable String groupName,
|
||||
@Nullable String systemContactName,
|
||||
|
@ -150,6 +152,7 @@ public class RecipientDetails {
|
|||
this.badges = record.getBadges();
|
||||
this.isReleaseChannel = isReleaseChannel;
|
||||
this.needsPniSignature = record.needsPniSignature();
|
||||
this.callLinkRoomId = record.getCallLinkRoomId();
|
||||
}
|
||||
|
||||
private RecipientDetails() {
|
||||
|
@ -203,8 +206,9 @@ public class RecipientDetails {
|
|||
this.hasGroupsInCommon = false;
|
||||
this.badges = Collections.emptyList();
|
||||
this.isReleaseChannel = false;
|
||||
this.needsPniSignature = false;
|
||||
this.isActiveGroup = false;
|
||||
this.needsPniSignature = false;
|
||||
this.isActiveGroup = false;
|
||||
this.callLinkRoomId = null;
|
||||
}
|
||||
|
||||
public static @NonNull RecipientDetails forIndividual(@NonNull Context context, @NonNull RecipientRecord settings) {
|
||||
|
@ -230,6 +234,10 @@ public class RecipientDetails {
|
|||
return new RecipientDetails(title, null, Optional.empty(), false, false, record.getRegistered(), record, members, false, false);
|
||||
}
|
||||
|
||||
public static @NonNull RecipientDetails forCallLink(String name, @NonNull RecipientRecord record) {
|
||||
return new RecipientDetails(name, null, Optional.empty(), false, false, record.getRegistered(), record, Collections.emptyList(), false, false);
|
||||
}
|
||||
|
||||
public static @NonNull RecipientDetails forUnknown() {
|
||||
return new RecipientDetails();
|
||||
}
|
||||
|
|
|
@ -52,8 +52,8 @@ public class IdleActionProcessor extends WebRtcActionProcessor {
|
|||
Log.i(TAG, "handleOutgoingCall():");
|
||||
|
||||
Recipient recipient = Recipient.resolved(remotePeer.getId());
|
||||
if (recipient.isGroup()) {
|
||||
Log.w(TAG, "Aborting attempt to start 1:1 call for group recipient: " + remotePeer.getId());
|
||||
if (recipient.isGroup() || recipient.isCallLink()) {
|
||||
Log.w(TAG, "Aborting attempt to start 1:1 call for group or call link recipient: " + remotePeer.getId());
|
||||
return currentState;
|
||||
}
|
||||
|
||||
|
@ -65,7 +65,7 @@ public class IdleActionProcessor extends WebRtcActionProcessor {
|
|||
protected @NonNull WebRtcServiceState handlePreJoinCall(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
|
||||
Log.i(TAG, "handlePreJoinCall():");
|
||||
|
||||
boolean isGroupCall = remotePeer.getRecipient().isPushV2Group();
|
||||
boolean isGroupCall = remotePeer.getRecipient().isPushV2Group() || remotePeer.getRecipient().isCallLink();
|
||||
WebRtcActionProcessor processor = isGroupCall ? new GroupPreJoinActionProcessor(webRtcInteractor)
|
||||
: new PreJoinActionProcessor(webRtcInteractor);
|
||||
|
||||
|
|
|
@ -8,14 +8,39 @@ package org.thoughtcrime.securesms.service.webrtc.links
|
|||
import android.os.Parcelable
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.signal.core.util.Serializer
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
|
||||
@Parcelize
|
||||
class CallLinkRoomId private constructor(private val roomId: ByteArray) : Parcelable {
|
||||
fun serialize(): String = Base64.encodeBytes(roomId)
|
||||
fun serialize(): String = DatabaseSerializer.serialize(this)
|
||||
|
||||
fun encodeForProto(): ByteString = ByteString.copyFrom(roomId)
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as CallLinkRoomId
|
||||
|
||||
if (!roomId.contentEquals(other.roomId)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return roomId.contentHashCode()
|
||||
}
|
||||
|
||||
object DatabaseSerializer : Serializer<CallLinkRoomId, String> {
|
||||
override fun serialize(data: CallLinkRoomId): String {
|
||||
return Base64.encodeBytes(data.roomId)
|
||||
}
|
||||
|
||||
override fun deserialize(data: String): CallLinkRoomId {
|
||||
return fromBytes(Base64.decode(data))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
|
|
15
app/src/main/res/drawable/symbol_link_compact_16.xml
Normal file
15
app/src/main/res/drawable/symbol_link_compact_16.xml
Normal file
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M10.84 8.42c-0.26 0.25-0.26 0.66 0 0.91 0.25 0.26 0.66 0.26 0.91 0L13.6 7.5c0.7-0.7 1.05-1.62 1.05-2.54 0-1.99-1.61-3.6-3.6-3.6-0.92 0-1.84 0.35-2.54 1.05L6.67 4.25C6.4 4.5 6.4 4.9 6.67 5.16c0.25 0.26 0.66 0.26 0.91 0l1.85-1.84c0.44-0.45 1.03-0.67 1.62-0.67 1.27 0 2.3 1.03 2.3 2.3 0 0.59-0.22 1.18-0.67 1.62l-1.84 1.85Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M10.46 5.54c0.25 0.25 0.25 0.67 0 0.92l-4 4c-0.25 0.25-0.67 0.25-0.92 0s-0.25-0.67 0-0.92l4-4c0.25-0.25 0.67-0.25 0.92 0Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M5.16 6.67c0.26 0.25 0.26 0.66 0 0.91L3.32 9.43c-0.45 0.44-0.67 1.03-0.67 1.62 0 1.27 1.03 2.3 2.3 2.3 0.59 0 1.18-0.22 1.62-0.67l1.85-1.84c0.25-0.26 0.66-0.26 0.91 0 0.26 0.25 0.26 0.66 0 0.91L7.5 13.6c-0.7 0.7-1.62 1.05-2.54 1.05-1.99 0-3.6-1.61-3.6-3.6 0-0.92 0.35-1.84 1.05-2.54l1.85-1.84C4.5 6.4 4.9 6.4 5.16 6.67Z"/>
|
||||
</vector>
|
|
@ -11,6 +11,11 @@
|
|||
<action
|
||||
android:id="@+id/action_callLinkDetailsFragment_to_editCallLinkNameDialogFragment"
|
||||
app:destination="@id/editCallLinkNameDialogFragment" />
|
||||
|
||||
<argument
|
||||
android:name="room_id"
|
||||
app:argType="org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId"
|
||||
app:nullable="false" />
|
||||
</fragment>
|
||||
|
||||
<dialog
|
||||
|
|
|
@ -1667,6 +1667,8 @@
|
|||
<string name="Recipient_you">You</string>
|
||||
<!-- Name of recipient representing user\'s \'My Story\' -->
|
||||
<string name="Recipient_my_story">My Story</string>
|
||||
<!-- Name of recipient for a call link without a name -->
|
||||
<string name="Recipient_signal_call">Signal call</string>
|
||||
|
||||
<!-- RecipientPreferencesActivity -->
|
||||
<string name="RecipientPreferenceActivity_block">Block</string>
|
||||
|
@ -5927,6 +5929,8 @@
|
|||
<string name="CallLogAdapter__return">Return</string>
|
||||
<!-- Call state template when there is more than one call collapsed into a single row. D is a number > 1 and S is a call info string (like Missed) -->
|
||||
<string name="CallLogAdapter__d_s">(%1$d) %2$s</string>
|
||||
<!-- Status text on call links -->
|
||||
<string name="CallLogAdapter__call_link">Call link</string>
|
||||
|
||||
<!-- Call Log context menu -->
|
||||
<!-- Displayed as a context menu item to start a video call -->
|
||||
|
@ -6072,6 +6076,12 @@
|
|||
<string name="CallLinkDetailsFragment__share_link">Share link</string>
|
||||
<!-- Displayed in a text row, allowing the user to delete the call link -->
|
||||
<string name="CallLinkDetailsFragment__delete_call_link">Delete call link</string>
|
||||
<!-- Displayed whenever a name change, revocation, etc, fails. -->
|
||||
<string name="CallLinkDetailsFragment__couldnt_save_changes">Couldn\'t save changes. Check your network connection and try again.</string>
|
||||
<!-- Displayed as title in dialog when user attempts to delete the link -->
|
||||
<string name="CallLinkDetailsFragment__delete_link">Delete link?</string>
|
||||
<!-- Displayed as body in dialog when user attempts to delete the link -->
|
||||
<string name="CallLinkDetailsFragment__this_link_will_no_longer_work">This link will no longer work for anyone who as it.</string>
|
||||
|
||||
<!-- Button label for the share button in the username link settings -->
|
||||
<string name="UsernameLinkSettings_share_button_label">Share</string>
|
||||
|
|
|
@ -153,7 +153,8 @@ object RecipientDatabaseTestUtils {
|
|||
hasGroupsInCommon,
|
||||
badges,
|
||||
needsPniSignature = false,
|
||||
isHidden = false
|
||||
isHidden = false,
|
||||
null
|
||||
),
|
||||
participants,
|
||||
isReleaseChannel,
|
||||
|
|
Loading…
Add table
Reference in a new issue