VoIP: use UserListFragment to select someone for call transfer (+ clean some code)

This commit is contained in:
ganfra 2020-12-22 16:32:32 +01:00
parent 22c981d8bf
commit 439ea42b54
32 changed files with 376 additions and 123 deletions

View file

@ -17,10 +17,18 @@
package im.vector.app.core.extensions
// Create a new Set including the provided element if not already present, or removing the element if already present
fun <T> Set<T>.toggle(element: T): Set<T> {
fun <T> Set<T>.toggle(element: T, singleElement: Boolean = false): Set<T> {
return if (contains(element)) {
if (singleElement) {
emptySet()
} else {
minus(element)
}
} else {
if (singleElement) {
setOf(element)
} else {
plus(element)
}
}
}

View file

@ -42,6 +42,7 @@ import androidx.lifecycle.ViewModelProvider
import androidx.viewbinding.ViewBinding
import com.bumptech.glide.util.Util
import com.google.android.material.snackbar.Snackbar
import com.jakewharton.rxbinding3.view.clicks
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
@ -84,6 +85,7 @@ import io.reactivex.disposables.Disposable
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.GlobalError
import timber.log.Timber
import java.util.concurrent.TimeUnit
import kotlin.system.measureTimeMillis
abstract class VectorBaseActivity<VB: ViewBinding> : AppCompatActivity(), HasScreenInjector {
@ -113,6 +115,19 @@ abstract class VectorBaseActivity<VB: ViewBinding> : AppCompatActivity(), HasScr
.disposeOnDestroy()
}
/* ==========================================================================================
* Views
* ========================================================================================== */
protected fun View.debouncedClicks(onClicked: () -> Unit) {
clicks()
.throttleFirst(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { onClicked() }
.disposeOnDestroy()
}
/* ==========================================================================================
* DATA
* ========================================================================================== */

View file

@ -63,6 +63,11 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC
dismiss()
}
views.callControlsTransfer.views.itemVerificationClickableZone.debouncedClicks {
callViewModel.handle(VectorCallViewActions.InitiateCallTransfer)
dismiss()
}
callViewModel.observeViewEvents {
when (it) {
is VectorCallViewEvents.ShowSoundDeviceChooser -> {
@ -153,5 +158,6 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC
views.callControlsToggleHoldResume.subTitle = null
views.callControlsToggleHoldResume.leftIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_call_hold_action)
}
views.callControlsTransfer.isVisible = state.canOpponentBeTransferred
}
}

View file

@ -320,6 +320,9 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
is VectorCallViewEvents.ConnectionTimeout -> {
onErrorTimoutConnect(event.turn)
}
is VectorCallViewEvents.ShowCallTransferScreen -> {
navigator.openCallTransfer(this, callArgs.callId)
}
null -> {
}
}

View file

@ -30,4 +30,5 @@ sealed class VectorCallViewActions : VectorViewModelAction {
object HeadSetButtonPressed : VectorCallViewActions()
object ToggleCamera : VectorCallViewActions()
object ToggleHDSD : VectorCallViewActions()
object InitiateCallTransfer : VectorCallViewActions()
}

View file

@ -27,6 +27,7 @@ sealed class VectorCallViewEvents : VectorViewEvents {
val available: List<CallAudioManager.SoundDevice>,
val current: CallAudioManager.SoundDevice
) : VectorCallViewEvents()
object ShowCallTransferScreen: VectorCallViewEvents()
// data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents()
// data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents()
// object CallAccepted : VectorCallViewEvents()

View file

@ -33,6 +33,7 @@ import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCall
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
import org.matrix.android.sdk.api.session.call.TurnServerResponse
import org.matrix.android.sdk.api.session.room.model.call.supportCallTransfer
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import java.util.Timer
@ -114,7 +115,8 @@ class VectorCallViewModel @AssistedInject constructor(
}
setState {
copy(
callState = Success(callState)
callState = Success(callState),
canOpponentBeTransferred = call.capabilities.supportCallTransfer()
)
}
}
@ -188,7 +190,8 @@ class VectorCallViewModel @AssistedInject constructor(
isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT,
canSwitchCamera = webRtcCall.canSwitchCamera(),
formattedDuration = webRtcCall.formattedDuration(),
isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD
isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD,
canOpponentBeTransferred = webRtcCall.mxCall.capabilities.supportCallTransfer()
)
}
updateOtherKnownCall(webRtcCall)
@ -269,6 +272,11 @@ class VectorCallViewModel @AssistedInject constructor(
if (!state.isVideoCall) return@withState
call?.setCaptureFormat(if (state.isHD) CaptureFormat.SD else CaptureFormat.HD)
}
VectorCallViewActions.InitiateCallTransfer -> {
_viewEvents.post(
VectorCallViewEvents.ShowCallTransferScreen
)
}
}.exhaustive
}

View file

@ -39,7 +39,8 @@ data class VectorCallViewState(
val callState: Async<CallState> = Uninitialized,
val otherKnownCallInfo: CallInfo? = null,
val callInfo: CallInfo = CallInfo(callId),
val formattedDuration: String = ""
val formattedDuration: String = "",
val canOpponentBeTransferred: Boolean = false
) : MvRxState {
data class CallInfo(

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.call.transfer
import im.vector.app.core.platform.VectorViewModelAction
sealed class CallTransferAction : VectorViewModelAction {
data class Connect(val consultFirst: Boolean, val selectedUserId: String) : CallTransferAction()
}

View file

@ -20,19 +20,20 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import android.widget.Toast
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.addFragment
import im.vector.app.core.extensions.addFragmentToBackstack
import im.vector.app.core.platform.SimpleFragmentActivity
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH
import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS
import im.vector.app.core.utils.allGranted
import im.vector.app.core.utils.checkPermissions
import im.vector.app.databinding.ActivityCallTransferBinding
import im.vector.app.features.contactsbook.ContactsBookFragment
import im.vector.app.features.contactsbook.ContactsBookViewModel
import im.vector.app.features.contactsbook.ContactsBookViewState
@ -48,7 +49,9 @@ import javax.inject.Inject
@Parcelize
data class CallTransferArgs(val callId: String) : Parcelable
class CallTransferActivity : SimpleFragmentActivity(), CallTransferViewModel.Factory, UserListViewModel.Factory, ContactsBookViewModel.Factory {
private const val USER_LIST_FRAGMENT_TAG = "USER_LIST_FRAGMENT_TAG"
class CallTransferActivity : VectorBaseActivity<ActivityCallTransferBinding>(), CallTransferViewModel.Factory, UserListViewModel.Factory, ContactsBookViewModel.Factory {
private lateinit var sharedActionViewModel: UserListSharedActionViewModel
@Inject lateinit var userListViewModelFactory: UserListViewModel.Factory
@ -56,6 +59,10 @@ class CallTransferActivity : SimpleFragmentActivity(), CallTransferViewModel.Fac
@Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter
private val callTransferViewModel: CallTransferViewModel by viewModel()
override fun getBinding() = ActivityCallTransferBinding.inflate(layoutInflater)
override fun injectWith(injector: ScreenComponent) {
super.injectWith(injector)
injector.inject(this)
@ -75,16 +82,11 @@ class CallTransferActivity : SimpleFragmentActivity(), CallTransferViewModel.Fac
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
views.toolbar.visibility = View.GONE
sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java)
sharedActionViewModel
.observe()
.subscribe { sharedAction ->
when (sharedAction) {
UserListSharedAction.Close -> finish()
UserListSharedAction.GoBack -> onBackPressed()
UserListSharedAction.OpenPhoneBook -> openPhoneBook()
// not exhaustive because it's a sharedAction
else -> {
@ -94,14 +96,36 @@ class CallTransferActivity : SimpleFragmentActivity(), CallTransferViewModel.Fac
.disposeOnDestroy()
if (isFirstCreation()) {
addFragment(
R.id.container,
R.id.callTransferFragmentContainer,
UserListFragment::class.java,
UserListFragmentArgs(
title = "Call transfer",
menuResId = -1
)
title = "",
menuResId = -1,
singleSelection = true,
showInviteActions = false,
showToolbar = false
),
USER_LIST_FRAGMENT_TAG
)
}
callTransferViewModel.observeViewEvents {
when (it) {
is CallTransferViewEvents.Dismiss -> finish()
}
}
configureToolbar(views.callTransferToolbar)
setupConnectAction()
}
private fun setupConnectAction() {
views.callTransferConnectAction.debouncedClicks {
val userListFragment = supportFragmentManager.findFragmentByTag(USER_LIST_FRAGMENT_TAG) as? UserListFragment
val selectedUser = userListFragment?.getCurrentState()?.getSelectedMatrixId()?.firstOrNull()
if (selectedUser != null) {
val action = CallTransferAction.Connect(views.callTransferConsultCheckBox.isChecked, selectedUser)
callTransferViewModel.handle(action)
}
}
}
private fun openPhoneBook() {
@ -110,7 +134,7 @@ class CallTransferActivity : SimpleFragmentActivity(), CallTransferViewModel.Fac
this,
PERMISSION_REQUEST_CODE_READ_CONTACTS,
0)) {
addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java)
addFragmentToBackstack(R.id.callTransferFragmentContainer, ContactsBookFragment::class.java)
}
}
@ -118,7 +142,7 @@ class CallTransferActivity : SimpleFragmentActivity(), CallTransferViewModel.Fac
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
doOnPostResume { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) }
doOnPostResume { addFragmentToBackstack(R.id.callTransferFragmentContainer, ContactsBookFragment::class.java) }
}
} else {
Toast.makeText(baseContext, R.string.missing_permissions_error, Toast.LENGTH_SHORT).show()

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.call.transfer
import im.vector.app.core.platform.VectorViewEvents
sealed class CallTransferViewEvents : VectorViewEvents {
object Dismiss : CallTransferViewEvents()
}

View file

@ -16,20 +16,23 @@
package im.vector.app.features.call.transfer
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.platform.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import org.matrix.android.sdk.api.session.Session
import im.vector.app.features.call.webrtc.WebRtcCall
import im.vector.app.features.call.webrtc.WebRtcCallManager
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCall
import timber.log.Timber
class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState)
: VectorViewModel<CallTransferViewState, EmptyAction, EmptyViewEvents>(initialState) {
class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState,
private val callManager: WebRtcCallManager)
: VectorViewModel<CallTransferViewState, CallTransferAction, CallTransferViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
@ -45,6 +48,43 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
}
}
override fun handle(action: EmptyAction) {}
private var call: WebRtcCall? = null
private val callListener = object : WebRtcCall.Listener {
override fun onStateUpdate(call: MxCall) {
if (call.state == CallState.Terminated) {
_viewEvents.post(CallTransferViewEvents.Dismiss)
}
}
}
init {
val webRtcCall = callManager.getCallById(initialState.callId)
if (webRtcCall == null) {
_viewEvents.post(CallTransferViewEvents.Dismiss)
} else {
call = webRtcCall
webRtcCall.addListener(callListener)
}
}
override fun onCleared() {
super.onCleared()
call?.removeListener(callListener)
}
override fun handle(action: CallTransferAction) {
when (action) {
is CallTransferAction.Connect -> transferCall(action)
}
}
private fun transferCall(action: CallTransferAction.Connect) {
viewModelScope.launch {
try {
call?.mxCall?.transfer(action.selectedUserId, null)
} catch (failure: Throwable) {
Timber.v("Fail to transfer call: $failure")
}
}
}
}

View file

@ -32,7 +32,7 @@ import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentContactsBookBinding
import im.vector.app.features.userdirectory.PendingInvitee
import im.vector.app.features.userdirectory.PendingSelection
import im.vector.app.features.userdirectory.UserListAction
import im.vector.app.features.userdirectory.UserListSharedAction
import im.vector.app.features.userdirectory.UserListSharedActionViewModel
@ -132,13 +132,13 @@ class ContactsBookFragment @Inject constructor(
override fun onMatrixIdClick(matrixId: String) {
view?.hideKeyboard()
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId))))
viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.UserPendingSelection(User(matrixId))))
sharedActionViewModel.post(UserListSharedAction.GoBack)
}
override fun onThreePidClick(threePid: ThreePid) {
view?.hideKeyboard()
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid)))
viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.ThreePidPendingSelection(threePid)))
sharedActionViewModel.post(UserListSharedAction.GoBack)
}
}

View file

@ -17,11 +17,11 @@
package im.vector.app.features.createdirect
import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.features.userdirectory.PendingInvitee
import im.vector.app.features.userdirectory.PendingSelection
sealed class CreateDirectRoomAction : VectorViewModelAction {
data class CreateRoomAndInviteSelectedUsers(
val invitees: Set<PendingInvitee>,
val selections: Set<PendingSelection>,
val existingDmRoomId: String?
) : CreateDirectRoomAction()
}

View file

@ -146,7 +146,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fac
private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) {
if (action.itemId == R.id.action_create_direct_room) {
viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(
action.invitees,
action.selections,
null
))
}

View file

@ -29,8 +29,7 @@ import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.databinding.FragmentQrCodeScannerBinding
import im.vector.app.features.userdirectory.PendingInvitee
import im.vector.app.features.userdirectory.PendingSelection
import me.dm7.barcodescanner.zxing.ZXingScannerView
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
@ -107,7 +106,7 @@ class CreateDirectRoomByQrCodeFragment @Inject constructor() : VectorBaseFragmen
val qrInvitee = if (viewModel.session.getUser(mxid) != null) viewModel.session.getUser(mxid)!! else User(mxid, null, null)
viewModel.handle(
CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(setOf(PendingInvitee.UserPendingInvitee(qrInvitee)), existingDm)
CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(setOf(PendingSelection.UserPendingSelection(qrInvitee)), existingDm)
)
}
}

View file

@ -26,10 +26,9 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.contactsbook.ContactsBookViewModel
import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault
import im.vector.app.features.userdirectory.PendingInvitee
import im.vector.app.features.userdirectory.PendingSelection
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.raw.RawService
@ -77,11 +76,11 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
}
} else {
// Create the DM
createRoomAndInviteSelectedUsers(action.invitees)
createRoomAndInviteSelectedUsers(action.selections)
}
}
private fun createRoomAndInviteSelectedUsers(invitees: Set<PendingInvitee>) {
private fun createRoomAndInviteSelectedUsers(selections: Set<PendingSelection>) {
viewModelScope.launch(Dispatchers.IO) {
val adminE2EByDefault = rawService.getElementWellknown(session.myUserId)
?.isE2EByDefault()
@ -89,10 +88,10 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
val roomParams = CreateRoomParams()
.apply {
invitees.forEach {
selections.forEach {
when (it) {
is PendingInvitee.UserPendingInvitee -> invitedUserIds.add(it.user.userId)
is PendingInvitee.ThreePidPendingInvitee -> invite3pids.add(it.threePid)
is PendingSelection.UserPendingSelection -> invitedUserIds.add(it.user.userId)
is PendingSelection.ThreePidPendingSelection -> invite3pids.add(it.threePid)
}.exhaustive
}
setDirectMessage()

View file

@ -17,8 +17,8 @@
package im.vector.app.features.invite
import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.features.userdirectory.PendingInvitee
import im.vector.app.features.userdirectory.PendingSelection
sealed class InviteUsersToRoomAction : VectorViewModelAction {
data class InviteSelectedUsers(val invitees: Set<PendingInvitee>) : InviteUsersToRoomAction()
data class InviteSelectedUsers(val selections: Set<PendingSelection>) : InviteUsersToRoomAction()
}

View file

@ -95,7 +95,6 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fa
}
.disposeOnDestroy()
if (isFirstCreation()) {
val args: InviteUsersToRoomArgs? = intent.extras?.getParcelable(MvRx.KEY_ARG)
addFragment(
R.id.container,
UserListFragment::class.java,
@ -103,7 +102,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fa
title = getString(R.string.invite_users_to_room_title),
menuResId = R.menu.vector_invite_users_to_room,
excludedUserIds = viewModel.getUserIdsOfRoomMembers(),
existingRoomId = args?.roomId
showInviteActions = false
)
)
}
@ -113,7 +112,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fa
private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) {
if (action.itemId == R.id.action_invite_users_to_room_invite) {
viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.invitees))
viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.selections))
}
}

View file

@ -25,8 +25,7 @@ import com.squareup.inject.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.contactsbook.ContactsBookViewModel
import im.vector.app.features.userdirectory.PendingInvitee
import im.vector.app.features.userdirectory.PendingSelection
import io.reactivex.Observable
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.rx.rx
@ -58,30 +57,30 @@ class InviteUsersToRoomViewModel @AssistedInject constructor(@Assisted
override fun handle(action: InviteUsersToRoomAction) {
when (action) {
is InviteUsersToRoomAction.InviteSelectedUsers -> inviteUsersToRoom(action.invitees)
is InviteUsersToRoomAction.InviteSelectedUsers -> inviteUsersToRoom(action.selections)
}
}
private fun inviteUsersToRoom(invitees: Set<PendingInvitee>) {
private fun inviteUsersToRoom(selections: Set<PendingSelection>) {
_viewEvents.post(InviteUsersToRoomViewEvents.Loading)
Observable.fromIterable(invitees).flatMapCompletable { user ->
Observable.fromIterable(selections).flatMapCompletable { user ->
when (user) {
is PendingInvitee.UserPendingInvitee -> room.rx().invite(user.user.userId, null)
is PendingInvitee.ThreePidPendingInvitee -> room.rx().invite3pid(user.threePid)
is PendingSelection.UserPendingSelection -> room.rx().invite(user.user.userId, null)
is PendingSelection.ThreePidPendingSelection -> room.rx().invite3pid(user.threePid)
}
}.subscribe(
{
val successMessage = when (invitees.size) {
val successMessage = when (selections.size) {
1 -> stringProvider.getString(R.string.invitation_sent_to_one_user,
invitees.first().getBestName())
selections.first().getBestName())
2 -> stringProvider.getString(R.string.invitations_sent_to_two_users,
invitees.first().getBestName(),
invitees.last().getBestName())
selections.first().getBestName(),
selections.last().getBestName())
else -> stringProvider.getQuantityString(R.plurals.invitations_sent_to_one_and_more_users,
invitees.size - 1,
invitees.first().getBestName(),
invitees.size - 1)
selections.size - 1,
selections.first().getBestName(),
selections.size - 1)
}
_viewEvents.post(InviteUsersToRoomViewEvents.Success(successMessage))
},

View file

@ -19,14 +19,14 @@ package im.vector.app.features.userdirectory
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.user.model.User
sealed class PendingInvitee {
data class UserPendingInvitee(val user: User) : PendingInvitee()
data class ThreePidPendingInvitee(val threePid: ThreePid) : PendingInvitee()
sealed class PendingSelection {
data class UserPendingSelection(val user: User) : PendingSelection()
data class ThreePidPendingSelection(val threePid: ThreePid) : PendingSelection()
fun getBestName(): String {
return when (this) {
is UserPendingInvitee -> user.getBestName()
is ThreePidPendingInvitee -> threePid.value
is UserPendingSelection -> user.getBestName()
is ThreePidPendingSelection -> threePid.value
}
}
}

View file

@ -21,7 +21,7 @@ import im.vector.app.core.platform.VectorViewModelAction
sealed class UserListAction : VectorViewModelAction {
data class SearchUsers(val value: String) : UserListAction()
object ClearSearchUsers : UserListAction()
data class SelectPendingInvitee(val pendingInvitee: PendingInvitee) : UserListAction()
data class RemovePendingInvitee(val pendingInvitee: PendingInvitee) : UserListAction()
data class AddPendingSelection(val pendingSelection: PendingSelection) : UserListAction()
data class RemovePendingSelection(val pendingSelection: PendingSelection) : UserListAction()
object ComputeMatrixToLinkForSharing : UserListAction()
}

View file

@ -54,10 +54,7 @@ class UserListController @Inject constructor(private val session: Session,
// Build generic items
if (currentState.searchTerm.isBlank()) {
// For now we remove this option if in invite to existing room flow (and not create DM)
if (currentState.pendingInvitees.isEmpty()
// For now we remove this option if in invite to existing room flow (and not create DM)
&& currentState.existingRoomId == null) {
if (currentState.showInviteActions()) {
actionItem {
id(R.drawable.ic_share)
title(stringProvider.getString(R.string.invite_friends))
@ -75,9 +72,7 @@ class UserListController @Inject constructor(private val session: Session,
callback?.onContactBookClick()
})
}
if (currentState.pendingInvitees.isEmpty()
// For now we remove this option if in invite to existing room flow (and not create DM)
&& currentState.existingRoomId == null) {
if (currentState.showInviteActions()) {
actionItem {
id(R.drawable.ic_qr_code_add)
title(stringProvider.getString(R.string.qr_code))

View file

@ -67,18 +67,22 @@ class UserListFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(UserListSharedActionViewModel::class.java)
if(args.showToolbar) {
views.userListTitle.text = args.title
vectorBaseActivity.setSupportActionBar(views.userListToolbar)
setupCloseView()
views.userListToolbar.isVisible = true
}else{
views.userListToolbar.isVisible = false
}
setupRecyclerView()
setupSearchView()
setupCloseView()
homeServerCapabilitiesViewModel.subscribe {
views.userListE2EbyDefaultDisabled.isVisible = !it.isE2EByDefault
}
viewModel.selectSubscribe(this, UserListViewState::pendingInvitees) {
viewModel.selectSubscribe(this, UserListViewState::pendingSelections) {
renderSelectedUsers(it)
}
@ -105,7 +109,7 @@ class UserListFragment @Inject constructor(
override fun onPrepareOptionsMenu(menu: Menu) {
withState(viewModel) {
val showMenuItem = it.pendingInvitees.isNotEmpty()
val showMenuItem = it.pendingSelections.isNotEmpty()
menu.forEach { menuItem ->
menuItem.isVisible = showMenuItem
}
@ -114,7 +118,7 @@ class UserListFragment @Inject constructor(
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) {
sharedActionViewModel.post(UserListSharedAction.OnMenuItemSelected(item.itemId, it.pendingInvitees))
sharedActionViewModel.post(UserListSharedAction.OnMenuItemSelected(item.itemId, it.pendingSelections))
return@withState true
}
@ -156,14 +160,14 @@ class UserListFragment @Inject constructor(
userListController.setData(it)
}
private fun renderSelectedUsers(invitees: Set<PendingInvitee>) {
private fun renderSelectedUsers(selections: Set<PendingSelection>) {
invalidateOptionsMenu()
val currentNumberOfChips = views.chipGroup.childCount
val newNumberOfChips = invitees.size
val newNumberOfChips = selections.size
views.chipGroup.removeAllViews()
invitees.forEach { addChipToGroup(it) }
selections.forEach { addChipToGroup(it) }
// Scroll to the bottom when adding chips. When removing chips, do not scroll
if (newNumberOfChips >= currentNumberOfChips) {
@ -173,20 +177,22 @@ class UserListFragment @Inject constructor(
}
}
private fun addChipToGroup(pendingInvitee: PendingInvitee) {
private fun addChipToGroup(pendingSelection: PendingSelection) {
val chip = Chip(requireContext())
chip.setChipBackgroundColorResource(android.R.color.transparent)
chip.chipStrokeWidth = dimensionConverter.dpToPx(1).toFloat()
chip.text = pendingInvitee.getBestName()
chip.text = pendingSelection.getBestName()
chip.isClickable = true
chip.isCheckable = false
chip.isCloseIconVisible = true
views.chipGroup.addView(chip)
chip.setOnCloseIconClickListener {
viewModel.handle(UserListAction.RemovePendingInvitee(pendingInvitee))
viewModel.handle(UserListAction.RemovePendingSelection(pendingSelection))
}
}
fun getCurrentState() = withState(viewModel){ it }
override fun onInviteFriendClick() {
viewModel.handle(UserListAction.ComputeMatrixToLinkForSharing)
}
@ -197,17 +203,17 @@ class UserListFragment @Inject constructor(
override fun onItemClick(user: User) {
view?.hideKeyboard()
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(user)))
viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.UserPendingSelection(user)))
}
override fun onMatrixIdClick(matrixId: String) {
view?.hideKeyboard()
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId))))
viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.UserPendingSelection(User(matrixId))))
}
override fun onThreePidClick(threePid: ThreePid) {
view?.hideKeyboard()
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid)))
viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.ThreePidPendingSelection(threePid)))
}
override fun onUseQRCode() {

View file

@ -24,5 +24,7 @@ data class UserListFragmentArgs(
val title: String,
val menuResId: Int,
val excludedUserIds: Set<String>? = null,
val existingRoomId: String? = null
val singleSelection: Boolean = false,
val showInviteActions: Boolean = true,
val showToolbar: Boolean = true
) : Parcelable

View file

@ -21,7 +21,7 @@ import im.vector.app.core.platform.VectorSharedAction
sealed class UserListSharedAction : VectorSharedAction {
object Close : UserListSharedAction()
object GoBack : UserListSharedAction()
data class OnMenuItemSelected(val itemId: Int, val invitees: Set<PendingInvitee>) : UserListSharedAction()
data class OnMenuItemSelected(val itemId: Int, val selections: Set<PendingSelection>) : UserListSharedAction()
object OpenPhoneBook : UserListSharedAction()
object AddByQrCode : UserListSharedAction()
}

View file

@ -68,12 +68,6 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
}
init {
setState {
copy(
myUserId = session.myUserId,
existingRoomId = initialState.existingRoomId
)
}
observeUsers()
}
@ -81,8 +75,8 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
when (action) {
is UserListAction.SearchUsers -> handleSearchUsers(action.value)
is UserListAction.ClearSearchUsers -> handleClearSearchUsers()
is UserListAction.SelectPendingInvitee -> handleSelectUser(action)
is UserListAction.RemovePendingInvitee -> handleRemoveSelectedUser(action)
is UserListAction.AddPendingSelection -> handleSelectUser(action)
is UserListAction.RemovePendingSelection -> handleRemoveSelectedUser(action)
UserListAction.ComputeMatrixToLinkForSharing -> handleShareMyMatrixToLink()
}.exhaustive
}
@ -168,13 +162,13 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
.disposeOnClear()
}
private fun handleSelectUser(action: UserListAction.SelectPendingInvitee) = withState { state ->
val selectedUsers = state.pendingInvitees.toggle(action.pendingInvitee)
setState { copy(pendingInvitees = selectedUsers) }
private fun handleSelectUser(action: UserListAction.AddPendingSelection) = withState { state ->
val selections = state.pendingSelections.toggle(action.pendingSelection, singleElement = state.singleSelection)
setState { copy(pendingSelections = selections) }
}
private fun handleRemoveSelectedUser(action: UserListAction.RemovePendingInvitee) = withState { state ->
val selectedUsers = state.pendingInvitees.minus(action.pendingInvitee)
setState { copy(pendingInvitees = selectedUsers) }
private fun handleRemoveSelectedUser(action: UserListAction.RemovePendingSelection) = withState { state ->
val selections = state.pendingSelections.minus(action.pendingSelection)
setState { copy(pendingSelections = selections) }
}
}

View file

@ -28,24 +28,28 @@ data class UserListViewState(
val knownUsers: Async<PagedList<User>> = Uninitialized,
val directoryUsers: Async<List<User>> = Uninitialized,
val filteredMappedContacts: List<MappedContact> = emptyList(),
val pendingInvitees: Set<PendingInvitee> = emptySet(),
val createAndInviteState: Async<String> = Uninitialized,
val pendingSelections: Set<PendingSelection> = emptySet(),
val searchTerm: String = "",
val myUserId: String = "",
val existingRoomId: String? = null
val singleSelection: Boolean,
private val showInviteActions: Boolean
) : MvRxState {
constructor(args: UserListFragmentArgs) : this(
existingRoomId = args.existingRoomId
singleSelection = args.singleSelection,
showInviteActions = args.showInviteActions
)
fun getSelectedMatrixId(): List<String> {
return pendingInvitees
return pendingSelections
.mapNotNull {
when (it) {
is PendingInvitee.UserPendingInvitee -> it.user.userId
is PendingInvitee.ThreePidPendingInvitee -> null
is PendingSelection.UserPendingSelection -> it.user.userId
is PendingSelection.ThreePidPendingSelection -> null
}
}
}
fun showInviteActions() = showInviteActions && pendingSelections.isEmpty()
}

View file

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<clip-path
android:pathData="M0,0h24v24h-24z"/>
<path
android:pathData="M8.027,15.9613C9.168,17.1932 11.9148,19.3263 12.6635,19.7641C12.7078,19.79 12.7585,19.8201 12.8152,19.8538C13.9576,20.5329 17.5373,22.6609 20.1454,20.6694C22.1661,19.1266 21.5091,17.3909 20.8289,16.875C20.3633,16.5128 18.9914,15.5145 17.7006,14.6152C16.4331,13.7322 15.7268,14.4397 15.2492,14.918C15.2404,14.9268 15.2317,14.9355 15.2231,14.9442L14.2621,15.9051C14.0174,16.1498 13.6451,16.0605 13.2886,15.7804C12.0092,14.8061 11.0681,13.8659 10.5972,13.395L10.5933,13.391C10.1225,12.9202 9.1939,11.9908 8.2196,10.7114C7.9395,10.3548 7.8502,9.9826 8.0949,9.7379L9.0559,8.7769C9.0645,8.7683 9.0732,8.7596 9.082,8.7508C9.5603,8.2732 10.2678,7.5668 9.3848,6.2994C8.4855,5.0086 7.4872,3.6367 7.125,3.1711C6.6091,2.4909 4.8734,1.8339 3.3306,3.8546C1.3391,6.4627 3.4671,10.0424 4.1462,11.1848C4.1799,11.2415 4.2101,11.2922 4.2359,11.3365C4.6737,12.0851 6.7951,14.8203 8.027,15.9613Z"
android:fillColor="#C1C6CD"/>
<path
android:pathData="M13.3084,9.7333C12.9179,10.1238 12.9179,10.757 13.3084,11.1475C13.6989,11.538 14.3321,11.538 14.7226,11.1475L20.4401,5.43L20.4401,9.9101C20.4401,10.4624 20.8878,10.9101 21.4401,10.9101C21.9924,10.9101 22.4401,10.4624 22.4401,9.9101L22.4401,3.0158C22.4401,2.4635 21.9924,2.0158 21.4401,2.0158H14.5458C13.9935,2.0158 13.5458,2.4635 13.5458,3.0158C13.5458,3.5681 13.9935,4.0158 14.5458,4.0158L19.0259,4.0158L13.3084,9.7333Z"
android:fillColor="#C1C6CD"
android:fillType="evenOdd"/>
</group>
</vector>

View file

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/vector_coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/callTransferToolbar"
style="@style/VectorToolbarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp"
app:layout_constraintTop_toTopOf="parent" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/callTransferFragmentContainer"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/callTransferActionsLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/callTransferToolbar" />
<RelativeLayout
android:background="?riotx_header_panel_background"
android:id="@+id/callTransferActionsLayout"
android:layout_width="match_parent"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintStart_toEndOf="parent"
android:layout_height="wrap_content">
<CheckBox
android:id="@+id/callTransferConsultCheckBox"
android:layout_width="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentStart="true"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/callTransferConsultTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/callTransferConsultCheckBox"
android:layout_toStartOf="@+id/callTransferConnectAction"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:text="@string/call_transfer_consult_first" />
<Button
android:id="@+id/callTransferConnectAction"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
android:text="@string/call_transfer_connect_action"
android:layout_height="wrap_content"/>
</RelativeLayout>
<include layout="@layout/merge_overlay_waiting_view" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -44,4 +44,13 @@
app:tint="?attr/riotx_text_primary"
tools:actionDescription="" />
<im.vector.app.core.ui.views.BottomSheetActionButton
android:id="@+id/callControlsTransfer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:actionTitle="@string/call_transfer_title"
app:leftIcon="@drawable/ic_call_transfer"
app:tint="?attr/riotx_text_primary"
tools:actionDescription="" />
</LinearLayout>

View file

@ -2782,5 +2782,9 @@
<string name="call_active_and_multiple_paused">1 active call (%1$s) · %2$d paused calls</string>
<string name="call_only_multiple_paused">%1$d paused calls</string>
<string name="call_transfer_consult_first">Consult first</string>
<string name="call_transfer_connect_action">Connect</string>
<string name="call_transfer_title">Transfer</string>
</resources>