Implement several parts of the call links admin UX.

This commit is contained in:
Alex Hart
2023-08-03 14:41:21 -03:00
parent b30f47bac4
commit d247e2c111
22 changed files with 1165 additions and 3 deletions

View File

@@ -6,7 +6,15 @@
package org.thoughtcrime.securesms.service.webrtc
import org.signal.core.util.logging.Log
import org.signal.ringrtc.CallException
import org.signal.ringrtc.GroupCall
import org.signal.ringrtc.PeekInfo
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState
import org.whispersystems.signalservice.api.push.ServiceId
/**
* Process actions for when the call link has at least once been connected and joined.
@@ -25,4 +33,67 @@ class CallLinkConnectedActionProcessor(
return currentState
}
override fun handleGroupJoinedMembershipChanged(currentState: WebRtcServiceState): WebRtcServiceState {
Log.i(tag, "handleGroupJoinedMembershipChanged():")
val superState: WebRtcServiceState = super.handleGroupJoinedMembershipChanged(currentState)
val groupCall: GroupCall = superState.callInfoState.requireGroupCall()
val peekInfo: PeekInfo = groupCall.peekInfo ?: return superState
val callLinkRoomId: CallLinkRoomId = superState.callInfoState.callRecipient.requireCallLinkRoomId()
val callLink: CallLinkTable.CallLink = SignalDatabase.callLinks.getCallLinkByRoomId(callLinkRoomId) ?: return superState
if (callLink.credentials?.adminPassBytes == null) {
Log.i(tag, "User is not an admin.")
return superState
}
Log.i(tag, "Updating pending list with ${peekInfo.pendingUsers.size} entries.")
val pendingParticipants: List<Recipient> = peekInfo.pendingUsers.map { Recipient.externalPush(ServiceId.ACI.from(it)) }
return superState.builder()
.changeCallInfoState()
.setPendingParticipants(pendingParticipants)
.build()
}
override fun handleSetCallLinkJoinRequestAccepted(currentState: WebRtcServiceState, participant: Recipient): WebRtcServiceState {
Log.i(tag, "handleSetCallLinkJoinRequestAccepted():")
val groupCall: GroupCall = currentState.callInfoState.requireGroupCall()
return try {
groupCall.approveUser(participant.requireAci().rawUuid)
currentState
.builder()
.changeCallInfoState()
.setPendingParticipantApproved(participant)
.build()
} catch (e: CallException) {
Log.w(tag, "Failed to approve user.", e)
currentState
}
}
override fun handleSetCallLinkJoinRequestRejected(currentState: WebRtcServiceState, participant: Recipient): WebRtcServiceState {
Log.i(tag, "handleSetCallLinkJoinRequestRejected():")
val groupCall: GroupCall = currentState.callInfoState.requireGroupCall()
return try {
groupCall.denyUser(participant.requireAci().rawUuid)
currentState
.builder()
.changeCallInfoState()
.setPendingParticipantRejected(participant)
.build()
} catch (e: CallException) {
Log.w(tag, "Failed to deny user.", e)
currentState
}
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.service.webrtc
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
/**
* Holds state information about what users wish to join a given call link.
*
* Also holds information about approvals and denials, so that this state is
* persisted over the lifecycle of the user's time in the call.
*/
data class PendingParticipantCollection(
private val participantMap: Map<RecipientId, Entry> = emptyMap(),
private val nowProvider: () -> Duration = { System.currentTimeMillis().milliseconds }
) {
/**
* Creates a new collection with the given recipients applied to it with the following rules:
*
* 1. If the recipient is already in the collection, ignore it
* 1. Otherwise, insert the recipient at the end of the colleciton in the pending state
* 1. Any recipients in the resulting collection that are [State.PENDING] and NOT in the passed recipient list are removed.
*/
fun withRecipients(recipients: List<Recipient>): PendingParticipantCollection {
val now = nowProvider()
val newEntries = recipients.filterNot { it.id in participantMap.keys }.map {
it.id to Entry(
it,
State.PENDING,
now
)
}
val recipientIds = recipients.map { it.id }
val newEntryMap = (participantMap + newEntries).filterNot { it.value.state == State.PENDING && it.key !in recipientIds }
return copy(participantMap = newEntryMap)
}
/**
* Creates a new collection with the given recipient marked as [State.APPROVED]
*/
fun withApproval(recipient: Recipient): PendingParticipantCollection {
val now = nowProvider()
val entry = Entry(
recipient = recipient,
state = State.APPROVED,
stateChangeAt = now
)
return copy(
participantMap = participantMap + (recipient.id to entry)
)
}
/**
* Creates a new collection with the given recipient marked as [State.DENIED]
*/
fun withDenial(recipient: Recipient): PendingParticipantCollection {
val now = nowProvider()
val entry = Entry(
recipient = recipient,
state = State.DENIED,
stateChangeAt = now
)
return copy(
participantMap = participantMap + (recipient.id to entry)
)
}
/**
* Gets all of the pending participants in the [State.PENDING] state.
*/
fun getUnresolvedPendingParticipants(): Set<Entry> {
return participantMap.values.filter { it.state == State.PENDING }.toSet()
}
/**
* Gets all of the pending participants regardless of state. Filterable
* via a 'since' parameter so that we only display non-[State.PENDING] entries with
* state change timestamps AFTER that parameter. [State.PENDING] entries will always
* be returned.
*
* @param since A timestamp, for which we will only return non-[State.PENDING] occurring afterwards.
*/
fun getAllPendingParticipants(since: Duration): Set<Entry> {
return participantMap.values.filter {
it.state == State.PENDING || it.stateChangeAt > since
}.toSet()
}
/**
* A [Recipient] and some [State] metadata
*/
data class Entry(
val recipient: Recipient,
val state: State,
val stateChangeAt: Duration
)
/**
* The state of a given recipient's approval
*/
enum class State {
/**
* No action has been taken
*/
PENDING,
/**
* The user has approved this recipient
*/
APPROVED,
/**
* The user has denied this recipient
*/
DENIED
}
}

View File

@@ -354,6 +354,22 @@ private void processStateless(@NonNull Function1<WebRtcEphemeralState, WebRtcEph
process((s, p) -> p.handleDropCall(s, callId));
}
public void setCallLinkJoinRequestAccepted(@NonNull Recipient participant) {
process((s, p) -> p.handleSetCallLinkJoinRequestAccepted(s, participant));
}
public void setCallLinkJoinRequestRejected(@NonNull Recipient participant) {
process((s, p) -> p.handleSetCallLinkJoinRequestRejected(s, participant));
}
public void removeFromCallLink(@NonNull Recipient participant) {
process((s, p) -> p.handleRemoveFromCallLink(s, participant));
}
public void blockFromCallLink(@NonNull Recipient participant) {
process((s, p) -> p.handleBlockFromCallLink(s, participant));
}
public void peekCallLinkCall(@NonNull RecipientId id) {
if (callManager == null) {
Log.i(TAG, "Unable to peekCallLinkCall, call manager is null");

View File

@@ -874,4 +874,32 @@ public abstract class WebRtcActionProcessor {
}
//endregion
//region Call Links
protected @NonNull WebRtcServiceState handleSetCallLinkJoinRequestAccepted(@NonNull WebRtcServiceState currentState, @NonNull Recipient participant) {
Log.i(tag, "handleSetCallLinkJoinRequestAccepted not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleSetCallLinkJoinRequestRejected(@NonNull WebRtcServiceState currentState, @NonNull Recipient participant) {
Log.i(tag, "handleSetCallLinkJoinRequestRejected not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleRemoveFromCallLink(@NonNull WebRtcServiceState currentState, @NonNull Recipient participant) {
Log.i(tag, "handleRemoveFromCallLink not processed");
return currentState;
}
protected @NonNull WebRtcServiceState handleBlockFromCallLink(@NonNull WebRtcServiceState currentState, @NonNull Recipient participant) {
Log.i(tag, "handleBlockFromCallLink not processed");
return currentState;
}
//endregion
}

View File

@@ -9,9 +9,12 @@ import org.thoughtcrime.securesms.events.WebRtcViewModel
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.ringrtc.RemotePeer
import org.thoughtcrime.securesms.service.webrtc.PendingParticipantCollection
/**
* General state of ongoing calls.
*
* @param pendingParticipants A list of pending users wishing to join a given call link.
*/
data class CallInfoState(
var callState: WebRtcViewModel.State = WebRtcViewModel.State.IDLE,
@@ -24,7 +27,8 @@ data class CallInfoState(
@get:JvmName("getGroupCallState") var groupState: WebRtcViewModel.GroupCallState = WebRtcViewModel.GroupCallState.IDLE,
var identityChangedRecipients: MutableSet<RecipientId> = mutableSetOf(),
var remoteDevicesCount: OptionalLong = OptionalLong.empty(),
var participantLimit: Long? = null
var participantLimit: Long? = null,
var pendingParticipants: PendingParticipantCollection = PendingParticipantCollection()
) {
val remoteCallParticipants: List<CallParticipant>

View File

@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.webrtc.PeerConnection;
import java.util.Collection;
import java.util.List;
import java.util.Set;
/**
@@ -342,5 +343,20 @@ public class WebRtcServiceStateBuilder {
toBuild.setParticipantLimit(participantLimit);
return this;
}
public @NonNull CallInfoStateBuilder setPendingParticipants(@NonNull List<Recipient> pendingParticipants) {
toBuild.setPendingParticipants(toBuild.getPendingParticipants().withRecipients(pendingParticipants));
return this;
}
public @NonNull CallInfoStateBuilder setPendingParticipantApproved(@NonNull Recipient participant) {
toBuild.setPendingParticipants(toBuild.getPendingParticipants().withApproval(participant));
return this;
}
public @NonNull CallInfoStateBuilder setPendingParticipantRejected(@NonNull Recipient participant) {
toBuild.setPendingParticipants(toBuild.getPendingParticipants().withDenial(participant));
return this;
}
}
}