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 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 // 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)) { return if (contains(element)) {
minus(element) if (singleElement) {
emptySet()
} else {
minus(element)
}
} else { } else {
plus(element) if (singleElement) {
setOf(element)
} else {
plus(element)
}
} }
} }

View file

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

View file

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

View file

@ -187,7 +187,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
views.callConnectingProgress.isVisible = true views.callConnectingProgress.isVisible = true
configureCallInfo(state) configureCallInfo(state)
} }
is CallState.Connected -> { is CallState.Connected -> {
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
if (state.isLocalOnHold || state.isRemoteOnHold) { if (state.isLocalOnHold || state.isRemoteOnHold) {
views.smallIsHeldIcon.isVisible = true views.smallIsHeldIcon.isVisible = true
@ -227,10 +227,10 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
views.callConnectingProgress.isVisible = true views.callConnectingProgress.isVisible = true
} }
} }
is CallState.Terminated -> { is CallState.Terminated -> {
finish() finish()
} }
null -> { null -> {
} }
} }
} }
@ -320,6 +320,9 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
is VectorCallViewEvents.ConnectionTimeout -> { is VectorCallViewEvents.ConnectionTimeout -> {
onErrorTimoutConnect(event.turn) onErrorTimoutConnect(event.turn)
} }
is VectorCallViewEvents.ShowCallTransferScreen -> {
navigator.openCallTransfer(this, callArgs.callId)
}
null -> { null -> {
} }
} }

View file

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

View file

@ -27,6 +27,7 @@ sealed class VectorCallViewEvents : VectorViewEvents {
val available: List<CallAudioManager.SoundDevice>, val available: List<CallAudioManager.SoundDevice>,
val current: CallAudioManager.SoundDevice val current: CallAudioManager.SoundDevice
) : VectorCallViewEvents() ) : VectorCallViewEvents()
object ShowCallTransferScreen: VectorCallViewEvents()
// data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents() // data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents()
// data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents() // data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents()
// object CallAccepted : 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.MxCall
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState 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.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.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import java.util.Timer import java.util.Timer
@ -114,7 +115,8 @@ class VectorCallViewModel @AssistedInject constructor(
} }
setState { setState {
copy( copy(
callState = Success(callState) callState = Success(callState),
canOpponentBeTransferred = call.capabilities.supportCallTransfer()
) )
} }
} }
@ -188,7 +190,8 @@ class VectorCallViewModel @AssistedInject constructor(
isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT, isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT,
canSwitchCamera = webRtcCall.canSwitchCamera(), canSwitchCamera = webRtcCall.canSwitchCamera(),
formattedDuration = webRtcCall.formattedDuration(), 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) updateOtherKnownCall(webRtcCall)
@ -269,6 +272,11 @@ class VectorCallViewModel @AssistedInject constructor(
if (!state.isVideoCall) return@withState if (!state.isVideoCall) return@withState
call?.setCaptureFormat(if (state.isHD) CaptureFormat.SD else CaptureFormat.HD) call?.setCaptureFormat(if (state.isHD) CaptureFormat.SD else CaptureFormat.HD)
} }
VectorCallViewActions.InitiateCallTransfer -> {
_viewEvents.post(
VectorCallViewEvents.ShowCallTransferScreen
)
}
}.exhaustive }.exhaustive
} }

View file

@ -39,7 +39,8 @@ data class VectorCallViewState(
val callState: Async<CallState> = Uninitialized, val callState: Async<CallState> = Uninitialized,
val otherKnownCallInfo: CallInfo? = null, val otherKnownCallInfo: CallInfo? = null,
val callInfo: CallInfo = CallInfo(callId), val callInfo: CallInfo = CallInfo(callId),
val formattedDuration: String = "" val formattedDuration: String = "",
val canOpponentBeTransferred: Boolean = false
) : MvRxState { ) : MvRxState {
data class CallInfo( 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.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.View
import android.widget.Toast import android.widget.Toast
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.ScreenComponent import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragment
import im.vector.app.core.extensions.addFragmentToBackstack 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.PERMISSIONS_FOR_MEMBERS_SEARCH
import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS
import im.vector.app.core.utils.allGranted import im.vector.app.core.utils.allGranted
import im.vector.app.core.utils.checkPermissions 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.ContactsBookFragment
import im.vector.app.features.contactsbook.ContactsBookViewModel import im.vector.app.features.contactsbook.ContactsBookViewModel
import im.vector.app.features.contactsbook.ContactsBookViewState import im.vector.app.features.contactsbook.ContactsBookViewState
@ -48,7 +49,9 @@ import javax.inject.Inject
@Parcelize @Parcelize
data class CallTransferArgs(val callId: String) : Parcelable 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 private lateinit var sharedActionViewModel: UserListSharedActionViewModel
@Inject lateinit var userListViewModelFactory: UserListViewModel.Factory @Inject lateinit var userListViewModelFactory: UserListViewModel.Factory
@ -56,6 +59,10 @@ class CallTransferActivity : SimpleFragmentActivity(), CallTransferViewModel.Fac
@Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory @Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter @Inject lateinit var errorFormatter: ErrorFormatter
private val callTransferViewModel: CallTransferViewModel by viewModel()
override fun getBinding() = ActivityCallTransferBinding.inflate(layoutInflater)
override fun injectWith(injector: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
super.injectWith(injector) super.injectWith(injector)
injector.inject(this) injector.inject(this)
@ -75,33 +82,50 @@ class CallTransferActivity : SimpleFragmentActivity(), CallTransferViewModel.Fac
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
views.toolbar.visibility = View.GONE
sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java) sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java)
sharedActionViewModel sharedActionViewModel
.observe() .observe()
.subscribe { sharedAction -> .subscribe { sharedAction ->
when (sharedAction) { when (sharedAction) {
UserListSharedAction.Close -> finish() UserListSharedAction.OpenPhoneBook -> openPhoneBook()
UserListSharedAction.GoBack -> onBackPressed()
UserListSharedAction.OpenPhoneBook -> openPhoneBook()
// not exhaustive because it's a sharedAction // not exhaustive because it's a sharedAction
else -> { else -> {
} }
} }
} }
.disposeOnDestroy() .disposeOnDestroy()
if (isFirstCreation()) { if (isFirstCreation()) {
addFragment( addFragment(
R.id.container, R.id.callTransferFragmentContainer,
UserListFragment::class.java, UserListFragment::class.java,
UserListFragmentArgs( UserListFragmentArgs(
title = "Call transfer", title = "",
menuResId = -1 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() { private fun openPhoneBook() {
@ -110,7 +134,7 @@ class CallTransferActivity : SimpleFragmentActivity(), CallTransferViewModel.Fac
this, this,
PERMISSION_REQUEST_CODE_READ_CONTACTS, PERMISSION_REQUEST_CODE_READ_CONTACTS,
0)) { 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) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (allGranted(grantResults)) { if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) { if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
doOnPostResume { addFragmentToBackstack(R.id.container, ContactsBookFragment::class.java) } doOnPostResume { addFragmentToBackstack(R.id.callTransferFragmentContainer, ContactsBookFragment::class.java) }
} }
} else { } else {
Toast.makeText(baseContext, R.string.missing_permissions_error, Toast.LENGTH_SHORT).show() Toast.makeText(baseContext, R.string.missing_permissions_error, Toast.LENGTH_SHORT).show()
@ -126,7 +150,7 @@ class CallTransferActivity : SimpleFragmentActivity(), CallTransferViewModel.Fac
} }
companion object { companion object {
fun newIntent(context: Context, callId: String): Intent { fun newIntent(context: Context, callId: String): Intent {
return Intent(context, CallTransferActivity::class.java).also { return Intent(context, CallTransferActivity::class.java).also {
it.putExtra(MvRx.KEY_ARG, CallTransferArgs(callId)) it.putExtra(MvRx.KEY_ARG, CallTransferArgs(callId))

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 package im.vector.app.features.call.transfer
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject 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.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.features.call.webrtc.WebRtcCall
import org.matrix.android.sdk.api.session.Session 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 import timber.log.Timber
class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState) class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState,
: VectorViewModel<CallTransferViewState, EmptyAction, EmptyViewEvents>(initialState) { private val callManager: WebRtcCallManager)
: VectorViewModel<CallTransferViewState, CallTransferAction, CallTransferViewEvents>(initialState) {
@AssistedInject.Factory @AssistedInject.Factory
interface 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.extensions.hideKeyboard
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentContactsBookBinding 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.UserListAction
import im.vector.app.features.userdirectory.UserListSharedAction import im.vector.app.features.userdirectory.UserListSharedAction
import im.vector.app.features.userdirectory.UserListSharedActionViewModel import im.vector.app.features.userdirectory.UserListSharedActionViewModel
@ -132,13 +132,13 @@ class ContactsBookFragment @Inject constructor(
override fun onMatrixIdClick(matrixId: String) { override fun onMatrixIdClick(matrixId: String) {
view?.hideKeyboard() view?.hideKeyboard()
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId)))) viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.UserPendingSelection(User(matrixId))))
sharedActionViewModel.post(UserListSharedAction.GoBack) sharedActionViewModel.post(UserListSharedAction.GoBack)
} }
override fun onThreePidClick(threePid: ThreePid) { override fun onThreePidClick(threePid: ThreePid) {
view?.hideKeyboard() view?.hideKeyboard()
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid))) viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.ThreePidPendingSelection(threePid)))
sharedActionViewModel.post(UserListSharedAction.GoBack) sharedActionViewModel.post(UserListSharedAction.GoBack)
} }
} }

View file

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

View file

@ -146,7 +146,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fac
private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) { private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) {
if (action.itemId == R.id.action_create_direct_room) { if (action.itemId == R.id.action_create_direct_room) {
viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers( viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(
action.invitees, action.selections,
null 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.checkPermissions
import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.databinding.FragmentQrCodeScannerBinding 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 me.dm7.barcodescanner.zxing.ZXingScannerView
import org.matrix.android.sdk.api.session.permalinks.PermalinkData import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser 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) val qrInvitee = if (viewModel.session.getUser(mxid) != null) viewModel.session.getUser(mxid)!! else User(mxid, null, null)
viewModel.handle( 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 com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel 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.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault 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.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.raw.RawService
@ -77,11 +76,11 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
} }
} else { } else {
// Create the DM // 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) { viewModelScope.launch(Dispatchers.IO) {
val adminE2EByDefault = rawService.getElementWellknown(session.myUserId) val adminE2EByDefault = rawService.getElementWellknown(session.myUserId)
?.isE2EByDefault() ?.isE2EByDefault()
@ -89,10 +88,10 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
val roomParams = CreateRoomParams() val roomParams = CreateRoomParams()
.apply { .apply {
invitees.forEach { selections.forEach {
when (it) { when (it) {
is PendingInvitee.UserPendingInvitee -> invitedUserIds.add(it.user.userId) is PendingSelection.UserPendingSelection -> invitedUserIds.add(it.user.userId)
is PendingInvitee.ThreePidPendingInvitee -> invite3pids.add(it.threePid) is PendingSelection.ThreePidPendingSelection -> invite3pids.add(it.threePid)
}.exhaustive }.exhaustive
} }
setDirectMessage() setDirectMessage()

View file

@ -17,8 +17,8 @@
package im.vector.app.features.invite package im.vector.app.features.invite
import im.vector.app.core.platform.VectorViewModelAction 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 { 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() .disposeOnDestroy()
if (isFirstCreation()) { if (isFirstCreation()) {
val args: InviteUsersToRoomArgs? = intent.extras?.getParcelable(MvRx.KEY_ARG)
addFragment( addFragment(
R.id.container, R.id.container,
UserListFragment::class.java, UserListFragment::class.java,
@ -103,7 +102,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fa
title = getString(R.string.invite_users_to_room_title), title = getString(R.string.invite_users_to_room_title),
menuResId = R.menu.vector_invite_users_to_room, menuResId = R.menu.vector_invite_users_to_room,
excludedUserIds = viewModel.getUserIdsOfRoomMembers(), excludedUserIds = viewModel.getUserIdsOfRoomMembers(),
existingRoomId = args?.roomId showInviteActions = false
) )
) )
} }
@ -113,7 +112,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fa
private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) { private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) {
if (action.itemId == R.id.action_invite_users_to_room_invite) { 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.R
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.contactsbook.ContactsBookViewModel import im.vector.app.features.userdirectory.PendingSelection
import im.vector.app.features.userdirectory.PendingInvitee
import io.reactivex.Observable import io.reactivex.Observable
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
@ -58,30 +57,30 @@ class InviteUsersToRoomViewModel @AssistedInject constructor(@Assisted
override fun handle(action: InviteUsersToRoomAction) { override fun handle(action: InviteUsersToRoomAction) {
when (action) { 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) _viewEvents.post(InviteUsersToRoomViewEvents.Loading)
Observable.fromIterable(invitees).flatMapCompletable { user -> Observable.fromIterable(selections).flatMapCompletable { user ->
when (user) { when (user) {
is PendingInvitee.UserPendingInvitee -> room.rx().invite(user.user.userId, null) is PendingSelection.UserPendingSelection -> room.rx().invite(user.user.userId, null)
is PendingInvitee.ThreePidPendingInvitee -> room.rx().invite3pid(user.threePid) is PendingSelection.ThreePidPendingSelection -> room.rx().invite3pid(user.threePid)
} }
}.subscribe( }.subscribe(
{ {
val successMessage = when (invitees.size) { val successMessage = when (selections.size) {
1 -> stringProvider.getString(R.string.invitation_sent_to_one_user, 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, 2 -> stringProvider.getString(R.string.invitations_sent_to_two_users,
invitees.first().getBestName(), selections.first().getBestName(),
invitees.last().getBestName()) selections.last().getBestName())
else -> stringProvider.getQuantityString(R.plurals.invitations_sent_to_one_and_more_users, else -> stringProvider.getQuantityString(R.plurals.invitations_sent_to_one_and_more_users,
invitees.size - 1, selections.size - 1,
invitees.first().getBestName(), selections.first().getBestName(),
invitees.size - 1) selections.size - 1)
} }
_viewEvents.post(InviteUsersToRoomViewEvents.Success(successMessage)) _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.identity.ThreePid
import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.session.user.model.User
sealed class PendingInvitee { sealed class PendingSelection {
data class UserPendingInvitee(val user: User) : PendingInvitee() data class UserPendingSelection(val user: User) : PendingSelection()
data class ThreePidPendingInvitee(val threePid: ThreePid) : PendingInvitee() data class ThreePidPendingSelection(val threePid: ThreePid) : PendingSelection()
fun getBestName(): String { fun getBestName(): String {
return when (this) { return when (this) {
is UserPendingInvitee -> user.getBestName() is UserPendingSelection -> user.getBestName()
is ThreePidPendingInvitee -> threePid.value is ThreePidPendingSelection -> threePid.value
} }
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -21,7 +21,7 @@ import im.vector.app.core.platform.VectorSharedAction
sealed class UserListSharedAction : VectorSharedAction { sealed class UserListSharedAction : VectorSharedAction {
object Close : UserListSharedAction() object Close : UserListSharedAction()
object GoBack : 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 OpenPhoneBook : UserListSharedAction()
object AddByQrCode : UserListSharedAction() object AddByQrCode : UserListSharedAction()
} }

View file

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

View file

@ -28,24 +28,28 @@ data class UserListViewState(
val knownUsers: Async<PagedList<User>> = Uninitialized, val knownUsers: Async<PagedList<User>> = Uninitialized,
val directoryUsers: Async<List<User>> = Uninitialized, val directoryUsers: Async<List<User>> = Uninitialized,
val filteredMappedContacts: List<MappedContact> = emptyList(), val filteredMappedContacts: List<MappedContact> = emptyList(),
val pendingInvitees: Set<PendingInvitee> = emptySet(), val pendingSelections: Set<PendingSelection> = emptySet(),
val createAndInviteState: Async<String> = Uninitialized,
val searchTerm: String = "", val searchTerm: String = "",
val myUserId: String = "", val myUserId: String = "",
val existingRoomId: String? = null val singleSelection: Boolean,
private val showInviteActions: Boolean
) : MvRxState { ) : MvRxState {
constructor(args: UserListFragmentArgs) : this( constructor(args: UserListFragmentArgs) : this(
existingRoomId = args.existingRoomId singleSelection = args.singleSelection,
showInviteActions = args.showInviteActions
) )
fun getSelectedMatrixId(): List<String> { fun getSelectedMatrixId(): List<String> {
return pendingInvitees return pendingSelections
.mapNotNull { .mapNotNull {
when (it) { when (it) {
is PendingInvitee.UserPendingInvitee -> it.user.userId is PendingSelection.UserPendingSelection -> it.user.userId
is PendingInvitee.ThreePidPendingInvitee -> null 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" app:tint="?attr/riotx_text_primary"
tools:actionDescription="" /> 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> </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_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_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> </resources>