DialPad: integrate getThirdPartyUser with protocol and add helper for create DM

This commit is contained in:
ganfra 2021-01-12 20:10:52 +01:00
parent 268d740059
commit 14502573bf
16 changed files with 298 additions and 82 deletions

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.thirdparty
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser
import org.matrix.android.sdk.api.util.Cancelable
interface ThirdPartyService {
@ -28,4 +29,11 @@ interface ThirdPartyService {
*/
suspend fun getThirdPartyProtocols(): Map<String, ThirdPartyProtocol>
/**
* Retrieve a Matrix User ID linked to a user on the third party service, given a set of user parameters.
* @param protocol Required. The name of the protocol.
* @param fields One or more custom fields that are passed to the AS to help identify the user.
*/
suspend fun getThirdPartyUser(protocol: String, fields: Map<String, String> = emptyMap()): List<ThirdPartyUser>
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2021 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 org.matrix.android.sdk.api.session.thirdparty.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.util.JsonDict
@JsonClass(generateAdapter = true)
data class ThirdPartyUser(
/*
Required. A Matrix User ID represting a third party user.
*/
@Json(name = "userid") val userId: String,
/*
Required. The protocol ID that the third party location is a part of.
*/
@Json(name = "protocol") val protocol: String,
/*
Required. Information used to identify this third party location.
*/
@Json(name = "fields") val fields: JsonDict
)

View File

@ -18,13 +18,22 @@ package org.matrix.android.sdk.internal.session.thirdparty
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService
import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser
import javax.inject.Inject
internal class DefaultThirdPartyService @Inject constructor(private val getThirdPartyProtocolTask: GetThirdPartyProtocolsTask)
internal class DefaultThirdPartyService @Inject constructor(private val getThirdPartyProtocolTask: GetThirdPartyProtocolsTask,
private val getThirdPartyUserTask: GetThirdPartyUserTask)
: ThirdPartyService {
override suspend fun getThirdPartyProtocols(): Map<String, ThirdPartyProtocol> {
return getThirdPartyProtocolTask.execute(Unit)
}
override suspend fun getThirdPartyUser(protocol: String, fields: Map<String, String>): List<ThirdPartyUser> {
val taskParams = GetThirdPartyUserTask.Params(
protocol = protocol,
fields = fields
)
return getThirdPartyUserTask.execute(taskParams)
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.thirdparty
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface GetThirdPartyUserTask : Task<GetThirdPartyUserTask.Params, List<ThirdPartyUser>> {
data class Params(
val protocol: String,
val fields: Map<String, String> = emptyMap()
)
}
internal class DefaultGetThirdPartyUserTask @Inject constructor(
private val thirdPartyAPI: ThirdPartyAPI,
private val eventBus: EventBus
) : GetThirdPartyUserTask {
override suspend fun execute(params: GetThirdPartyUserTask.Params): List<ThirdPartyUser> {
return executeRequest(eventBus) {
apiCall = thirdPartyAPI.getThirdPartyUser(params.protocol, params.fields)
}
}
}

View File

@ -21,6 +21,7 @@ import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.network.NetworkConstants
import org.matrix.android.sdk.internal.session.room.alias.GetAliasesResponse
@ -47,15 +48,28 @@ import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
import retrofit2.http.QueryMap
internal interface ThirdPartyAPI {
/**
* Get the third party server protocols.
*
* Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-thirdparty-protocols
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1.html#get-matrix-client-r0-thirdparty-protocols
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols")
fun thirdPartyProtocols(): Call<Map<String, ThirdPartyProtocol>>
/**
* Retrieve a Matrix User ID linked to a user on the third party service, given a set of user parameters.
*
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-thirdparty-user-protocol
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols/user/{protocol}")
fun getThirdPartyUser(@Path("protocol") protocol: String, @QueryMap params: Map<String, String>?): Call<List<ThirdPartyUser>>
}

View File

@ -42,4 +42,7 @@ internal abstract class ThirdPartyModule {
@Binds
abstract fun bindGetThirdPartyProtocolsTask(task: DefaultGetThirdPartyProtocolsTask): GetThirdPartyProtocolsTask
@Binds
abstract fun bindGetThirdPartyUserTask(task: DefaultGetThirdPartyUserTask): GetThirdPartyUserTask
}

View File

@ -18,6 +18,7 @@ package im.vector.app.core.error
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.call.dialpad.DialPadLookup
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.failure.isInvalidPassword
@ -109,8 +110,9 @@ class DefaultErrorFormatter @Inject constructor(
throwable.localizedMessage
}
}
is SsoFlowNotSupportedYet -> stringProvider.getString(R.string.error_sso_flow_not_supported_yet)
else -> throwable.localizedMessage
is SsoFlowNotSupportedYet -> stringProvider.getString(R.string.error_sso_flow_not_supported_yet)
is DialPadLookup.Failure -> stringProvider.getString(R.string.call_dial_pad_lookup_error)
else -> throwable.localizedMessage
}
?: stringProvider.getString(R.string.unknown_error)
}

View File

@ -63,7 +63,7 @@ class CallDialPadBottomSheet private constructor() : VectorBaseBottomSheetDialog
putBoolean(DialPadFragment.EXTRA_ENABLE_OK, showActions)
putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country)
}
callback = this@CallDialPadBottomSheet.callback
callback = DialPadFragmentCallbackWrapper(this@CallDialPadBottomSheet.callback)
}.also {
addChildFragment(R.id.callDialPadFragmentContainer, it)
}
@ -83,6 +83,24 @@ class CallDialPadBottomSheet private constructor() : VectorBaseBottomSheetDialog
private fun setCallbackToFragment(callback: DialPadFragment.Callback?) {
if (!isAdded) return
val dialPadFragment = childFragmentManager.findFragmentById(R.id.callDialPadFragmentContainer) as? DialPadFragment
dialPadFragment?.callback = callback
dialPadFragment?.callback = DialPadFragmentCallbackWrapper(callback)
}
private inner class DialPadFragmentCallbackWrapper(val callback: DialPadFragment.Callback?): DialPadFragment.Callback{
override fun onDigitAppended(digit: String) {
callback?.onDigitAppended(digit)
}
override fun onOkClicked(formatted: String?, raw: String?) {
callback?.onOkClicked(formatted, raw)
dismiss()
}
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (c) 2021 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.dialpad
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.createdirect.DirectRoomHelper
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session
class DialPadLookup(val session: Session,
val directRoomHelper: DirectRoomHelper,
val callManager: WebRtcCallManager
) {
class Failure : Throwable()
data class Result(val userId: String, val roomId: String)
suspend fun lookupPhoneNumber(phoneNumber: String): Result? {
val supportedProtocolKey = callManager.supportedPSTNProtocol ?: return null
val thirdPartyUser = tryOrNull {
session.getThirdPartyUser(supportedProtocolKey, fields = mapOf(
"m.id.phone" to phoneNumber
)).firstOrNull()
} ?: throw Failure()
val roomId = directRoomHelper.ensureDMExists(thirdPartyUser.userId)
return Result(userId = thirdPartyUser.userId, roomId = roomId)
}
}

View File

@ -100,17 +100,20 @@ class WebRtcCallManager @Inject constructor(
private var peerConnectionFactory: PeerConnectionFactory? = null
private val executor = Executors.newSingleThreadExecutor()
private val dispatcher = executor.asCoroutineDispatcher()
var supportsPSTNProtocol: Boolean = false
var supportedPSTNProtocol: String? = null
private set
val supportsPSTNProtocol: Boolean
get() = supportedPSTNProtocol != null
private val rootEglBase by lazy { EglUtils.rootEglBase }
private var isInBackground: Boolean = true
init {
GlobalScope.launch {
supportsPSTNProtocol = currentSession?.getSupportedPSTN(3) != null
if (supportsPSTNProtocol) {
supportedPSTNProtocol = currentSession?.getSupportedPSTN(3)
if (supportedPSTNProtocol != null) {
pstnSupportListeners.forEach { it.onPSTNSupportUpdated() }
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) 2021 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.createdirect
import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.internal.util.awaitCallback
import java.lang.IllegalStateException
import javax.inject.Inject
class DirectRoomHelper @Inject constructor(
private val rawService: RawService,
private val session: Session
) {
suspend fun ensureDMExists(userId: String): String {
val existingRoomId = tryOrNull { session.getExistingDirectRoomWithUser(userId) }
val roomId: String
if (existingRoomId != null) {
roomId = existingRoomId
} else {
val adminE2EByDefault = rawService.getElementWellknown(session.myUserId)
?.isE2EByDefault()
?: true
val roomParams = CreateRoomParams().apply {
invitedUserIds.add(userId)
setDirectMessage()
enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault
}
roomId = awaitCallback {
session.createRoom(roomParams, it)
}
}
return roomId
}
}

View File

@ -72,6 +72,8 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class IgnoreUser(val userId: String?) : RoomDetailAction()
object ResendAll : RoomDetailAction()
data class StartCallWithPhoneNumber(val phoneNumber: String, val videoCall: Boolean): RoomDetailAction()
data class StartCall(val isVideo: Boolean) : RoomDetailAction()
data class AcceptCall(val callId: String): RoomDetailAction()
object EndCall : RoomDetailAction()

View File

@ -32,9 +32,11 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.subscribeLogError
import im.vector.app.features.call.dialpad.DialPadLookup
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.command.CommandParser
import im.vector.app.features.command.ParsedCommand
import im.vector.app.features.createdirect.DirectRoomHelper
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
@ -115,6 +117,7 @@ class RoomDetailViewModel @AssistedInject constructor(
private val typingHelper: TypingHelper,
private val callManager: WebRtcCallManager,
private val chatEffectManager: ChatEffectManager,
private val directRoomHelper: DirectRoomHelper,
timelineSettingsFactory: TimelineSettingsFactory
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), Timeline.Listener, ChatEffectManager.Delegate {
@ -264,6 +267,7 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action)
is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
is RoomDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action)
is RoomDetailAction.StartCall -> handleStartCall(action)
is RoomDetailAction.AcceptCall -> handleAcceptCall(action)
is RoomDetailAction.EndCall -> handleEndCall()
@ -287,6 +291,17 @@ class RoomDetailViewModel @AssistedInject constructor(
}.exhaustive
}
private fun handleStartCallWithPhoneNumber(action: RoomDetailAction.StartCallWithPhoneNumber) {
viewModelScope.launch {
try {
val result = DialPadLookup(session, directRoomHelper, callManager).lookupPhoneNumber(action.phoneNumber) ?: return@launch
callManager.startOutgoingCall(result.roomId, result.userId, action.videoCall)
} catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure))
}
}
}
private fun handleAcceptCall(action: RoomDetailAction.AcceptCall) {
callManager.getCallById(action.callId)?.also {
_viewEvents.post(RoomDetailViewEvents.DisplayAndAcceptCall(it))
@ -317,18 +332,15 @@ class RoomDetailViewModel @AssistedInject constructor(
}
private fun handleOpenOrCreateDm(action: RoomDetailAction.OpenOrCreateDm) {
val existingDmRoomId = session.getExistingDirectRoomWithUser(action.userId)
if (existingDmRoomId == null) {
// First create a direct room
viewModelScope.launch(Dispatchers.IO) {
val roomId = awaitCallback<String> {
session.createDirectRoom(action.userId, it)
}
_viewEvents.post(RoomDetailViewEvents.OpenRoom(roomId))
viewModelScope.launch {
val roomId = try {
directRoomHelper.ensureDMExists(action.userId)
} catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure))
return@launch
}
} else {
if (existingDmRoomId != initialState.roomId) {
_viewEvents.post(RoomDetailViewEvents.OpenRoom(existingDmRoomId))
if (roomId != initialState.roomId) {
_viewEvents.post(RoomDetailViewEvents.OpenRoom(roomId = roomId))
}
}
}

View File

@ -30,6 +30,7 @@ import im.vector.app.R
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.createdirect.DirectRoomHelper
import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault
import kotlinx.coroutines.Dispatchers
@ -48,6 +49,7 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor(
@Assisted initialState: MatrixToBottomSheetState,
private val session: Session,
private val stringProvider: StringProvider,
private val directRoomHelper: DirectRoomHelper,
private val rawService: RawService) : VectorViewModel<MatrixToBottomSheetState, MatrixToAction, MatrixToViewEvents>(initialState) {
@AssistedInject.Factory
@ -125,42 +127,23 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor(
}
private fun handleStartChatting(action: MatrixToAction.StartChattingWithUser) {
val mxId = action.matrixItem.id
val existing = session.getExistingDirectRoomWithUser(mxId)
if (existing != null) {
// navigate to this room
_viewEvents.post(MatrixToViewEvents.NavigateToRoom(existing))
} else {
viewModelScope.launch {
setState {
copy(startChattingState = Loading())
}
// we should create the room then navigate
viewModelScope.launch(Dispatchers.IO) {
val adminE2EByDefault = rawService.getElementWellknown(session.myUserId)
?.isE2EByDefault()
?: true
val roomParams = CreateRoomParams()
.apply {
invitedUserIds.add(mxId)
setDirectMessage()
enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault
}
val roomId = try {
awaitCallback<String> { session.createRoom(roomParams, it) }
} catch (failure: Throwable) {
setState {
copy(startChattingState = Fail(Exception(stringProvider.getString(R.string.invite_users_to_room_failure))))
}
return@launch
}
val roomId = try {
directRoomHelper.ensureDMExists(action.matrixItem.id)
} catch (failure: Throwable) {
setState {
// we can hide this button has we will navigate out
copy(startChattingState = Uninitialized)
copy(startChattingState = Fail(Exception(stringProvider.getString(R.string.invite_users_to_room_failure))))
}
_viewEvents.post(MatrixToViewEvents.NavigateToRoom(roomId))
return@launch
}
setState {
// we can hide this button has we will navigate out
copy(startChattingState = Uninitialized)
}
_viewEvents.post(MatrixToViewEvents.NavigateToRoom(roomId))
}
}
}

View File

@ -26,8 +26,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.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault
import im.vector.app.features.createdirect.DirectRoomHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
@ -35,7 +34,6 @@ import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session
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.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.internal.util.awaitCallback
@ -44,6 +42,7 @@ class UserCodeSharedViewModel @AssistedInject constructor(
@Assisted val initialState: UserCodeState,
private val session: Session,
private val stringProvider: StringProvider,
private val directRoomHelper: DirectRoomHelper,
private val rawService: RawService) : VectorViewModel<UserCodeState, UserCodeActions, UserCodeShareViewEvents>(initialState) {
companion object : MvRxViewModelFactory<UserCodeSharedViewModel, UserCodeState> {
@ -95,39 +94,20 @@ class UserCodeSharedViewModel @AssistedInject constructor(
private fun handleStartChatting(withUser: UserCodeActions.StartChattingWithUser) {
val mxId = withUser.matrixItem.id
val existing = session.getExistingDirectRoomWithUser(mxId)
setState {
copy(mode = UserCodeState.Mode.SHOW)
}
if (existing != null) {
// navigate to this room
_viewEvents.post(UserCodeShareViewEvents.NavigateToRoom(existing))
} else {
// we should create the room then navigate
_viewEvents.post(UserCodeShareViewEvents.ShowWaitingScreen)
viewModelScope.launch(Dispatchers.IO) {
val adminE2EByDefault = rawService.getElementWellknown(session.myUserId)
?.isE2EByDefault()
?: true
val roomParams = CreateRoomParams()
.apply {
invitedUserIds.add(mxId)
setDirectMessage()
enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault
}
val roomId =
try {
awaitCallback<String> { session.createRoom(roomParams, it) }
} catch (failure: Throwable) {
_viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.invite_users_to_room_failure)))
return@launch
} finally {
_viewEvents.post(UserCodeShareViewEvents.HideWaitingScreen)
}
_viewEvents.post(UserCodeShareViewEvents.NavigateToRoom(roomId))
_viewEvents.post(UserCodeShareViewEvents.ShowWaitingScreen)
viewModelScope.launch(Dispatchers.IO) {
val roomId = try {
directRoomHelper.ensureDMExists(mxId)
} catch (failure: Throwable) {
_viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.invite_users_to_room_failure)))
return@launch
} finally {
_viewEvents.post(UserCodeShareViewEvents.HideWaitingScreen)
}
_viewEvents.post(UserCodeShareViewEvents.NavigateToRoom(roomId))
}
}

View File

@ -2774,4 +2774,7 @@
<string name="call_tile_ended">This call has ended</string>
<string name="call_tile_call_back">Call back</string>
<string name="call_dial_pad_title">Dial pad</string>
<string name="call_dial_pad_lookup_error">"There was an error looking up the phone number"</string>
</resources>