Call Link single-user request sheet.

This commit is contained in:
Alex Hart 2023-09-11 15:48:02 -03:00
parent e41accf52d
commit 92b0ebb6f6
7 changed files with 350 additions and 0 deletions

View file

@ -73,6 +73,7 @@ import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
import org.thoughtcrime.securesms.components.webrtc.WebRtcControls;
import org.thoughtcrime.securesms.components.webrtc.WifiToCellularPopupWindow;
import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
import org.thoughtcrime.securesms.components.webrtc.requests.CallLinkIncomingRequestSheet;
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
@ -1088,6 +1089,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
public void onLaunchPendingRequestsSheet() {
new PendingParticipantsBottomSheet().show(getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
}
@Override
public void onLaunchRecipientSheet(@NonNull Recipient pendingRecipient) {
CallLinkIncomingRequestSheet.show(getSupportFragmentManager(), pendingRecipient.getId());
}
}
private class WindowLayoutInfoConsumer implements Consumer<WindowLayoutInfo> {

View file

@ -53,6 +53,8 @@ class PendingParticipantsView @JvmOverloads constructor(
val firstRecipient: Recipient = unresolvedPendingParticipants.first()
avatar.setAvatar(firstRecipient)
avatar.setOnClickListener { listener?.onLaunchRecipientSheet(firstRecipient) }
name.text = firstRecipient.getShortDisplayName(context)
allow.setOnClickListener { listener?.onAllowPendingRecipient(firstRecipient) }
@ -70,6 +72,11 @@ class PendingParticipantsView @JvmOverloads constructor(
}
interface Listener {
/**
* Display the sheet containing the request for the top level participant
*/
fun onLaunchRecipientSheet(pendingRecipient: Recipient)
/**
* Given recipient should be admitted to the call
*/

View file

@ -0,0 +1,31 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.webrtc.requests
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.contacts.paged.GroupsInCommon
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
class CallLinkIncomingRequestRepository {
fun getGroupsInCommon(recipientId: RecipientId): Observable<GroupsInCommon> {
return Recipient.observable(recipientId).flatMapSingle { recipient ->
if (recipient.hasGroupsInCommon()) {
Single.fromCallable {
val groupsInCommon = SignalDatabase.groups.getGroupsContainingMember(recipient.id, true)
val total = groupsInCommon.size
val names = groupsInCommon.take(2).map { it.title!! }
GroupsInCommon(total, names)
}.observeOn(Schedulers.io())
} else {
Single.just(GroupsInCommon(0, listOf()))
}
}
}
}

View file

@ -0,0 +1,235 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.webrtc.requests
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rxjava3.subscribeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Dividers
import org.signal.core.ui.Rows
import org.signal.core.ui.theme.SignalTheme
import org.signal.core.util.getParcelableCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.viewModel
/**
* Displayed when the user presses the user avatar in the call link join request
* bar.
*/
class CallLinkIncomingRequestSheet : ComposeBottomSheetDialogFragment() {
companion object {
private const val RECIPIENT_ID = "recipient_id"
@JvmStatic
fun show(fragmentManager: FragmentManager, recipientId: RecipientId) {
CallLinkIncomingRequestSheet().apply {
arguments = bundleOf(
RECIPIENT_ID to recipientId
)
}.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
override fun isDarkTheme(): Boolean = true
private val recipientId: RecipientId by lazy {
requireArguments().getParcelableCompat(RECIPIENT_ID, RecipientId::class.java)!!
}
private val viewModel by viewModel {
CallLinkIncomingRequestViewModel(recipientId)
}
@Composable
override fun SheetContent() {
val state = viewModel.observeState(LocalContext.current).subscribeAsState(initial = CallLinkIncomingRequestState())
if (state.value.recipient == Recipient.UNKNOWN) {
return
}
CallLinkIncomingRequestSheetContent(
state = state.value,
onApproveEntry = this::onApproveEntry,
onDenyEntry = this::onDenyEntry
)
}
private fun onApproveEntry() {
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(recipientId)
dismissAllowingStateLoss()
}
private fun onDenyEntry() {
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(recipientId)
dismissAllowingStateLoss()
}
}
@Preview
@Composable
private fun CallLinkIncomingRequestSheetContentPreview() {
SignalTheme(isDarkMode = true) {
Surface {
CallLinkIncomingRequestSheetContent(
state = CallLinkIncomingRequestState(
name = "Miles Morales",
subtitle = "+1 (555) 555-5555",
groupsInCommon = "Member of Webheads",
isSystemContact = true
),
onApproveEntry = {},
onDenyEntry = {}
)
}
}
}
@Composable
private fun CallLinkIncomingRequestSheetContent(
state: CallLinkIncomingRequestState,
onApproveEntry: () -> Unit,
onDenyEntry: () -> Unit
) {
LazyColumn(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
item { BottomSheets.Handle() }
item { Avatar(state.recipient) }
item {
Title(
recipientName = state.name,
isSystemContact = state.isSystemContact
)
}
if (state.subtitle.isNotEmpty()) {
item {
Text(
text = state.subtitle,
modifier = Modifier.padding(4.dp)
)
}
}
if (state.groupsInCommon.isNotEmpty()) {
item {
Text(
text = state.groupsInCommon,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(6.dp)
)
}
}
item {
Dividers.Default()
}
item {
Rows.TextRow(
text = stringResource(id = R.string.CallLinkIncomingRequestSheet__approve_entry),
icon = ImageVector.vectorResource(R.drawable.symbol_check_circle_24),
onClick = onApproveEntry
)
}
item {
Rows.TextRow(
text = stringResource(id = R.string.CallLinkIncomingRequestSheet__deny_entry),
icon = ImageVector.vectorResource(R.drawable.symbol_x_circle_24),
onClick = onDenyEntry
)
}
item {
Spacer(modifier = Modifier.size(32.dp))
}
}
}
@Composable
private fun Avatar(
recipient: Recipient
) {
if (LocalInspectionMode.current) {
Spacer(
modifier = Modifier
.padding(top = 13.dp)
.size(80.dp)
.background(color = Color.Red, shape = CircleShape)
)
} else {
AndroidView(
factory = ::AvatarImageView,
modifier = Modifier
.size(80.dp)
.padding(top = 13.dp)
) {
it.setAvatarUsingProfile(recipient)
}
}
}
@Composable
private fun Title(
recipientName: String,
isSystemContact: Boolean
) {
if (isSystemContact) {
Row(modifier = Modifier.padding(top = 12.dp)) {
Text(
text = recipientName,
style = MaterialTheme.typography.headlineMedium
)
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_person_circle_24),
contentDescription = null,
modifier = Modifier
.padding(start = 6.dp)
.align(CenterVertically)
)
}
} else {
Text(
text = recipientName,
style = MaterialTheme.typography.headlineMedium
)
}
}

View file

@ -0,0 +1,17 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.webrtc.requests
import androidx.compose.runtime.Stable
import org.thoughtcrime.securesms.recipients.Recipient
data class CallLinkIncomingRequestState(
val recipient: Recipient = Recipient.UNKNOWN,
val name: String = "",
val isSystemContact: Boolean = false,
val subtitle: String = "",
@Stable val groupsInCommon: String = ""
)

View file

@ -0,0 +1,48 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.webrtc.requests
import android.content.Context
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.rx.RxStore
class CallLinkIncomingRequestViewModel(
private val recipientId: RecipientId
) : ViewModel() {
private val repository = CallLinkIncomingRequestRepository()
private val store = RxStore(CallLinkIncomingRequestState())
private val disposables = CompositeDisposable().apply {
add(store)
}
override fun onCleared() {
disposables.dispose()
}
fun observeState(context: Context): Flowable<CallLinkIncomingRequestState> {
disposables += store.update(Recipient.observable(recipientId).toFlowable(BackpressureStrategy.LATEST)) { r, s ->
s.copy(
recipient = r,
name = r.getShortDisplayName(context),
subtitle = r.e164.orElse(""),
isSystemContact = r.isSystemContact
)
}
disposables += store.update(repository.getGroupsInCommon(recipientId).toFlowable(BackpressureStrategy.LATEST)) { g, s ->
s.copy(groupsInCommon = g.toDisplayText(context))
}
return store.stateFlowable
}
}

View file

@ -6099,6 +6099,12 @@
<!-- Displayed when we copy the call link to the clipboard -->
<string name="CreateCallLinkBottomSheetDialogFragment__copied_to_clipboard">Copied to clipboard</string>
<!-- CallLinkIncomingRequestSheet -->
<!-- Displayed as line item in sheet for approving or denying a single user -->
<string name="CallLinkIncomingRequestSheet__approve_entry">Approve entry</string>
<!-- Displayed as line item in sheet for approving or denying a single user -->
<string name="CallLinkIncomingRequestSheet__deny_entry">Deny entry</string>
<!-- EditCallLinkNameDialogFragment -->
<!-- App bar title for editing a call name -->
<string name="EditCallLinkNameDialogFragment__edit_call_name">Edit call name</string>