mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-22 01:15:54 +03:00
Merge pull request #3383 from vector-im/feature/fga/authenticated_jitsi
Feature/fga/authenticated jitsi
This commit is contained in:
commit
b44c1a1c77
24 changed files with 578 additions and 173 deletions
|
@ -2,10 +2,10 @@ Changes in Element 1.1.8 (2021-XX-XX)
|
|||
===================================================
|
||||
|
||||
Features ✨:
|
||||
-
|
||||
-
|
||||
|
||||
Improvements 🙌:
|
||||
-
|
||||
- Support Jitsi authentication (#3379)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Space Invite by link not always displayed for public space (#3345)
|
||||
|
|
|
@ -39,6 +39,7 @@ import org.matrix.android.sdk.api.session.identity.IdentityService
|
|||
import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService
|
||||
import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService
|
||||
import org.matrix.android.sdk.api.session.media.MediaService
|
||||
import org.matrix.android.sdk.api.session.openid.OpenIdService
|
||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
|
||||
import org.matrix.android.sdk.api.session.profile.ProfileService
|
||||
import org.matrix.android.sdk.api.session.pushers.PushersService
|
||||
|
@ -233,6 +234,11 @@ interface Session :
|
|||
*/
|
||||
fun spaceService(): SpaceService
|
||||
|
||||
/**
|
||||
* Returns the open id service associated with the session
|
||||
*/
|
||||
fun openIdService(): OpenIdService
|
||||
|
||||
/**
|
||||
* Add a listener to the session.
|
||||
* @param listener the listener to add.
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.api.session.openid
|
||||
|
||||
interface OpenIdService {
|
||||
|
||||
/**
|
||||
* Gets an OpenID token object that the requester may supply to another service to verify their identity in Matrix.
|
||||
* The generated token is only valid for exchanging for user information from the federation API for OpenID.
|
||||
*/
|
||||
suspend fun getOpenIdToken(): OpenIdToken
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.api.session.openid
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OpenIdToken(
|
||||
/**
|
||||
* Required. An access token the consumer may use to verify the identity of the person who generated the token.
|
||||
* This is given to the federation API GET /openid/userinfo to verify the user's identity.
|
||||
*/
|
||||
@Json(name = "access_token")
|
||||
val accessToken: String,
|
||||
|
||||
/**
|
||||
* Required. The string "Bearer".
|
||||
*/
|
||||
@Json(name = "token_type")
|
||||
val tokenType: String,
|
||||
|
||||
/**
|
||||
* Required. The homeserver domain the consumer should use when attempting to verify the user's identity.
|
||||
*/
|
||||
@Json(name = "matrix_server_name")
|
||||
val matrixServerName: String,
|
||||
|
||||
/**
|
||||
* Required. The number of seconds before this token expires and a new one must be generated.
|
||||
*/
|
||||
@Json(name = "expires_in")
|
||||
val expiresIn: Int
|
||||
)
|
|
@ -43,6 +43,7 @@ import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesServi
|
|||
import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService
|
||||
import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService
|
||||
import org.matrix.android.sdk.api.session.media.MediaService
|
||||
import org.matrix.android.sdk.api.session.openid.OpenIdService
|
||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
|
||||
import org.matrix.android.sdk.api.session.profile.ProfileService
|
||||
import org.matrix.android.sdk.api.session.pushers.PushersService
|
||||
|
@ -125,6 +126,7 @@ internal class DefaultSession @Inject constructor(
|
|||
private val thirdPartyService: Lazy<ThirdPartyService>,
|
||||
private val callSignalingService: Lazy<CallSignalingService>,
|
||||
private val spaceService: Lazy<SpaceService>,
|
||||
private val openIdService: Lazy<OpenIdService>,
|
||||
@UnauthenticatedWithCertificate
|
||||
private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>
|
||||
) : Session,
|
||||
|
@ -289,6 +291,8 @@ internal class DefaultSession @Inject constructor(
|
|||
|
||||
override fun spaceService(): SpaceService = spaceService.get()
|
||||
|
||||
override fun openIdService(): OpenIdService = openIdService.get()
|
||||
|
||||
override fun getOkHttpClient(): OkHttpClient {
|
||||
return unauthenticatedWithCertificateOkHttpClient.get()
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.session.accountdata.AccountDataService
|
|||
import org.matrix.android.sdk.api.session.events.EventService
|
||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
|
||||
import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService
|
||||
import org.matrix.android.sdk.api.session.openid.OpenIdService
|
||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
|
||||
import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
|
||||
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
|
||||
|
@ -82,6 +83,7 @@ import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapab
|
|||
import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService
|
||||
import org.matrix.android.sdk.internal.session.initsync.DefaultInitialSyncProgressService
|
||||
import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager
|
||||
import org.matrix.android.sdk.internal.session.openid.DefaultOpenIdService
|
||||
import org.matrix.android.sdk.internal.session.permalinks.DefaultPermalinkService
|
||||
import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor
|
||||
import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor
|
||||
|
@ -373,6 +375,9 @@ internal abstract class SessionModule {
|
|||
@Binds
|
||||
abstract fun bindPermalinkService(service: DefaultPermalinkService): PermalinkService
|
||||
|
||||
@Binds
|
||||
abstract fun bindOpenIdTokenService(service: DefaultOpenIdService): OpenIdService
|
||||
|
||||
@Binds
|
||||
abstract fun bindTypingUsersTracker(tracker: DefaultTypingUsersTracker): TypingUsersTracker
|
||||
|
||||
|
|
|
@ -16,9 +16,9 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.identity
|
||||
|
||||
import org.matrix.android.sdk.api.session.openid.OpenIdToken
|
||||
import org.matrix.android.sdk.internal.network.NetworkConstants
|
||||
import org.matrix.android.sdk.internal.session.identity.model.IdentityRegisterResponse
|
||||
import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
|
@ -52,5 +52,5 @@ internal interface IdentityAuthAPI {
|
|||
* The request body is the same as the values returned by /openid/request_token in the Client-Server API.
|
||||
*/
|
||||
@POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "account/register")
|
||||
suspend fun register(@Body openIdToken: RequestOpenIdTokenResponse): IdentityRegisterResponse
|
||||
suspend fun register(@Body openIdToken: OpenIdToken): IdentityRegisterResponse
|
||||
}
|
||||
|
|
|
@ -16,16 +16,16 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.identity
|
||||
|
||||
import org.matrix.android.sdk.api.session.openid.OpenIdToken
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.session.identity.model.IdentityRegisterResponse
|
||||
import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface IdentityRegisterTask : Task<IdentityRegisterTask.Params, IdentityRegisterResponse> {
|
||||
data class Params(
|
||||
val identityAuthAPI: IdentityAuthAPI,
|
||||
val openIdTokenResponse: RequestOpenIdTokenResponse
|
||||
val openIdToken: OpenIdToken
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ internal class DefaultIdentityRegisterTask @Inject constructor() : IdentityRegis
|
|||
|
||||
override suspend fun execute(params: IdentityRegisterTask.Params): IdentityRegisterResponse {
|
||||
return executeRequest(null) {
|
||||
params.identityAuthAPI.register(params.openIdTokenResponse)
|
||||
params.identityAuthAPI.register(params.openIdToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2021 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.openid
|
||||
|
||||
import org.matrix.android.sdk.api.session.openid.OpenIdService
|
||||
import org.matrix.android.sdk.api.session.openid.OpenIdToken
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultOpenIdService @Inject constructor(private val getOpenIdTokenTask: GetOpenIdTokenTask): OpenIdService {
|
||||
|
||||
override suspend fun getOpenIdToken(): OpenIdToken {
|
||||
return getOpenIdTokenTask.execute(Unit)
|
||||
}
|
||||
}
|
|
@ -16,20 +16,21 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.openid
|
||||
|
||||
import org.matrix.android.sdk.api.session.openid.OpenIdToken
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface GetOpenIdTokenTask : Task<Unit, RequestOpenIdTokenResponse>
|
||||
internal interface GetOpenIdTokenTask : Task<Unit, OpenIdToken>
|
||||
|
||||
internal class DefaultGetOpenIdTokenTask @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
private val openIdAPI: OpenIdAPI,
|
||||
private val globalErrorReceiver: GlobalErrorReceiver) : GetOpenIdTokenTask {
|
||||
|
||||
override suspend fun execute(params: Unit): RequestOpenIdTokenResponse {
|
||||
override suspend fun execute(params: Unit): OpenIdToken {
|
||||
return executeRequest(globalErrorReceiver) {
|
||||
openIdAPI.openIdToken(userId)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.openid
|
||||
|
||||
import org.matrix.android.sdk.api.session.openid.OpenIdToken
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.internal.network.NetworkConstants
|
||||
import retrofit2.http.Body
|
||||
|
@ -34,5 +35,5 @@ internal interface OpenIdAPI {
|
|||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token")
|
||||
suspend fun openIdToken(@Path("userId") userId: String,
|
||||
@Body body: JsonDict = emptyMap()): RequestOpenIdTokenResponse
|
||||
@Body body: JsonDict = emptyMap()): OpenIdToken
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
package org.matrix.android.sdk.internal.session.widgets
|
||||
|
||||
import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse
|
||||
import org.matrix.android.sdk.api.session.openid.OpenIdToken
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
|
@ -29,7 +29,7 @@ internal interface WidgetsAPI {
|
|||
* @param body the body content (Ref: https://github.com/matrix-org/matrix-doc/pull/1961)
|
||||
*/
|
||||
@POST("register")
|
||||
suspend fun register(@Body body: RequestOpenIdTokenResponse,
|
||||
suspend fun register(@Body body: OpenIdToken,
|
||||
@Query("v") version: String?): RegisterWidgetResponse
|
||||
|
||||
@GET("account")
|
||||
|
|
|
@ -308,6 +308,7 @@ dependencies {
|
|||
def arch_version = '2.1.0'
|
||||
def lifecycle_version = '2.2.0'
|
||||
def rxbinding_version = '3.1.0'
|
||||
def jjwt_version = '0.11.2'
|
||||
|
||||
// Tests
|
||||
def kluent_version = '1.65'
|
||||
|
@ -449,9 +450,9 @@ dependencies {
|
|||
|
||||
// Jitsi
|
||||
implementation('org.jitsi.react:jitsi-meet-sdk:3.1.0') {
|
||||
exclude group: 'com.google.firebase'
|
||||
exclude group: 'com.google.android.gms'
|
||||
exclude group: 'com.android.installreferrer'
|
||||
exclude group: 'com.google.firebase'
|
||||
exclude group: 'com.google.android.gms'
|
||||
exclude group: 'com.android.installreferrer'
|
||||
}
|
||||
|
||||
// QR-code
|
||||
|
@ -465,6 +466,15 @@ dependencies {
|
|||
|
||||
implementation 'im.dlg:android-dialer:1.2.5'
|
||||
|
||||
// JWT
|
||||
api "io.jsonwebtoken:jjwt-api:$jjwt_version"
|
||||
runtimeOnly "io.jsonwebtoken:jjwt-impl:$jjwt_version"
|
||||
runtimeOnly("io.jsonwebtoken:jjwt-orgjson:$jjwt_version") {
|
||||
exclude group: 'org.json', module: 'json' //provided by Android natively
|
||||
}
|
||||
implementation 'commons-codec:commons-codec:1.15'
|
||||
|
||||
|
||||
// TESTS
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation "org.amshove.kluent:kluent-android:$kluent_version"
|
||||
|
|
47
vector/src/main/java/im/vector/app/core/network/OkHttp.kt
Normal file
47
vector/src/main/java/im/vector/app/core/network/OkHttp.kt
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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.core.network
|
||||
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
suspend fun Call.await(): Response {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
enqueue(object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
continuation.resume(response)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
if (continuation.isCancelled) return
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
})
|
||||
continuation.invokeOnCancellation {
|
||||
try {
|
||||
cancel()
|
||||
} catch (ex: Throwable) {
|
||||
// Ignore cancel exception
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
27
vector/src/main/java/im/vector/app/core/utils/Base32.kt
Normal file
27
vector/src/main/java/im/vector/app/core/utils/Base32.kt
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.core.utils
|
||||
|
||||
import org.apache.commons.codec.binary.Base32
|
||||
|
||||
fun String.toBase32String(padding: Boolean = true): String {
|
||||
val base32 = Base32().encodeAsString(toByteArray())
|
||||
return if (padding) {
|
||||
base32
|
||||
} else {
|
||||
base32.trimEnd('=')
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@ fun String.isValidUrl(): Boolean {
|
|||
/**
|
||||
* Ensure string starts with "http". If it is not the case, "https://" is added, only if the String is not empty
|
||||
*/
|
||||
internal fun String.ensureProtocol(): String {
|
||||
fun String.ensureProtocol(): String {
|
||||
return when {
|
||||
isEmpty() -> this
|
||||
!startsWith("http") -> "https://$this"
|
||||
|
@ -38,7 +38,7 @@ internal fun String.ensureProtocol(): String {
|
|||
}
|
||||
}
|
||||
|
||||
internal fun String.ensureTrailingSlash(): String {
|
||||
fun String.ensureTrailingSlash(): String {
|
||||
return when {
|
||||
isEmpty() -> this
|
||||
!endsWith("/") -> "$this/"
|
||||
|
|
|
@ -20,12 +20,13 @@ import im.vector.app.core.platform.VectorViewEvents
|
|||
import org.jitsi.meet.sdk.JitsiMeetUserInfo
|
||||
|
||||
sealed class JitsiCallViewEvents : VectorViewEvents {
|
||||
data class StartConference(
|
||||
data class JoinConference(
|
||||
val enableVideo: Boolean,
|
||||
val jitsiUrl: String,
|
||||
val subject: String,
|
||||
val confId: String,
|
||||
val userInfo: JitsiMeetUserInfo
|
||||
val userInfo: JitsiMeetUserInfo,
|
||||
val token: String?
|
||||
) : JitsiCallViewEvents()
|
||||
|
||||
data class ConfirmSwitchingConference(
|
||||
|
@ -33,5 +34,6 @@ sealed class JitsiCallViewEvents : VectorViewEvents {
|
|||
) : JitsiCallViewEvents()
|
||||
|
||||
object LeaveConference : JitsiCallViewEvents()
|
||||
object FailJoiningConference: JitsiCallViewEvents()
|
||||
object Finish : JitsiCallViewEvents()
|
||||
}
|
||||
|
|
|
@ -27,24 +27,19 @@ import dagger.assisted.AssistedFactory
|
|||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.features.themes.ThemeProvider
|
||||
import io.reactivex.disposables.Disposable
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jitsi.meet.sdk.JitsiMeetUserInfo
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import org.matrix.android.sdk.rx.asObservable
|
||||
import java.net.URL
|
||||
|
||||
class JitsiCallViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: JitsiCallViewState,
|
||||
private val session: Session,
|
||||
private val jitsiMeetPropertiesFactory: JitsiWidgetPropertiesFactory,
|
||||
private val themeProvider: ThemeProvider
|
||||
private val jitsiService: JitsiService
|
||||
) : VectorViewModel<JitsiCallViewState, JitsiCallViewActions, JitsiCallViewEvents>(initialState) {
|
||||
|
||||
@AssistedFactory
|
||||
|
@ -55,7 +50,7 @@ class JitsiCallViewModel @AssistedInject constructor(
|
|||
private var currentWidgetObserver: Disposable? = null
|
||||
private val widgetService = session.widgetService()
|
||||
|
||||
private var confIsStarted = false
|
||||
private var confIsJoined = false
|
||||
private var pendingArgs: VectorJitsiActivity.Args? = null
|
||||
|
||||
init {
|
||||
|
@ -63,7 +58,7 @@ class JitsiCallViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun observeWidget(roomId: String, widgetId: String) {
|
||||
confIsStarted = false
|
||||
confIsJoined = false
|
||||
currentWidgetObserver?.dispose()
|
||||
currentWidgetObserver = widgetService.getRoomWidgetsLive(roomId, QueryStringValue.Equals(widgetId), WidgetType.Jitsi.values())
|
||||
.asObservable()
|
||||
|
@ -74,10 +69,9 @@ class JitsiCallViewModel @AssistedInject constructor(
|
|||
setState {
|
||||
copy(widget = Success(jitsiWidget))
|
||||
}
|
||||
|
||||
if (!confIsStarted) {
|
||||
confIsStarted = true
|
||||
startConference(jitsiWidget)
|
||||
if (!confIsJoined) {
|
||||
confIsJoined = true
|
||||
joinConference(jitsiWidget)
|
||||
}
|
||||
} else {
|
||||
setState {
|
||||
|
@ -90,24 +84,15 @@ class JitsiCallViewModel @AssistedInject constructor(
|
|||
.disposeOnClear()
|
||||
}
|
||||
|
||||
private fun startConference(jitsiWidget: Widget) = withState { state ->
|
||||
val me = session.getRoomMember(session.myUserId, state.roomId)?.toMatrixItem()
|
||||
val userInfo = JitsiMeetUserInfo().apply {
|
||||
displayName = me?.getBestName()
|
||||
avatar = me?.avatarUrl?.let { session.contentUrlResolver().resolveFullSize(it) }?.let { URL(it) }
|
||||
private fun joinConference(jitsiWidget: Widget) = withState { state ->
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val joinConference = jitsiService.joinConference(state.roomId, jitsiWidget, state.enableVideo)
|
||||
_viewEvents.post(joinConference)
|
||||
} catch (throwable: Throwable) {
|
||||
_viewEvents.post(JitsiCallViewEvents.FailJoiningConference)
|
||||
}
|
||||
}
|
||||
val roomName = session.getRoomSummary(state.roomId)?.displayName
|
||||
|
||||
val ppt = widgetService.getWidgetComputedUrl(jitsiWidget, themeProvider.isLightTheme())
|
||||
?.let { url -> jitsiMeetPropertiesFactory.create(url) }
|
||||
|
||||
_viewEvents.post(JitsiCallViewEvents.StartConference(
|
||||
enableVideo = state.enableVideo,
|
||||
jitsiUrl = "https://${ppt?.domain}",
|
||||
subject = roomName ?: "",
|
||||
confId = ppt?.confId ?: "",
|
||||
userInfo = userInfo
|
||||
))
|
||||
}
|
||||
|
||||
override fun handle(action: JitsiCallViewActions) {
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* 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.conference
|
||||
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.network.await
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.utils.ensureProtocol
|
||||
import im.vector.app.core.utils.toBase32String
|
||||
import im.vector.app.features.call.conference.jwt.JitsiJWTFactory
|
||||
import im.vector.app.features.raw.wellknown.getElementWellknown
|
||||
import im.vector.app.features.settings.VectorLocale
|
||||
import im.vector.app.features.themes.ThemeProvider
|
||||
import okhttp3.Request
|
||||
import org.jitsi.meet.sdk.JitsiMeetUserInfo
|
||||
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.widgets.model.Widget
|
||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||
import org.matrix.android.sdk.api.util.appendParamToUrl
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import java.net.URL
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
class JitsiService @Inject constructor(
|
||||
private val session: Session,
|
||||
private val rawService: RawService,
|
||||
private val stringProvider: StringProvider,
|
||||
private val themeProvider: ThemeProvider,
|
||||
private val jitsiWidgetPropertiesFactory: JitsiWidgetPropertiesFactory,
|
||||
private val jitsiJWTFactory: JitsiJWTFactory) {
|
||||
|
||||
companion object {
|
||||
const val JITSI_OPEN_ID_TOKEN_JWT_AUTH = "openidtoken-jwt"
|
||||
private const val JITSI_AUTH_KEY = "auth"
|
||||
}
|
||||
|
||||
suspend fun createJitsiWidget(roomId: String, withVideo: Boolean): Widget {
|
||||
// Build data for a jitsi widget
|
||||
val widgetId: String = WidgetType.Jitsi.preferred + "_" + session.myUserId + "_" + System.currentTimeMillis()
|
||||
val preferredJitsiDomain = tryOrNull {
|
||||
rawService.getElementWellknown(session.myUserId)
|
||||
?.jitsiServer
|
||||
?.preferredDomain
|
||||
}
|
||||
val jitsiDomain = preferredJitsiDomain ?: stringProvider.getString(R.string.preferred_jitsi_domain)
|
||||
val jitsiAuth = getJitsiAuth(jitsiDomain)
|
||||
val confId = createConferenceId(roomId, jitsiAuth)
|
||||
|
||||
// We use the default element wrapper for this widget
|
||||
// https://github.com/vector-im/element-web/blob/develop/docs/jitsi-dev.md
|
||||
// https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/utils/WidgetUtils.ts#L469
|
||||
val url = buildString {
|
||||
append("https://app.element.io/jitsi.html")
|
||||
appendParamToUrl("confId", confId)
|
||||
append("#conferenceDomain=\$domain")
|
||||
append("&conferenceId=\$conferenceId")
|
||||
append("&isAudioOnly=\$isAudioOnly")
|
||||
append("&displayName=\$matrix_display_name")
|
||||
append("&avatarUrl=\$matrix_avatar_url")
|
||||
append("&userId=\$matrix_user_id")
|
||||
append("&roomId=\$matrix_room_id")
|
||||
append("&theme=\$theme")
|
||||
if (jitsiAuth != null) {
|
||||
append("&auth=$jitsiAuth")
|
||||
}
|
||||
}
|
||||
val widgetEventContent = mapOf(
|
||||
"url" to url,
|
||||
"type" to WidgetType.Jitsi.legacy,
|
||||
"data" to mapOf(
|
||||
"conferenceId" to confId,
|
||||
"domain" to jitsiDomain,
|
||||
"isAudioOnly" to !withVideo,
|
||||
JITSI_AUTH_KEY to jitsiAuth
|
||||
),
|
||||
"creatorUserId" to session.myUserId,
|
||||
"id" to widgetId,
|
||||
"name" to "jitsi"
|
||||
)
|
||||
|
||||
return session.widgetService().createRoomWidget(roomId, widgetId, widgetEventContent)
|
||||
}
|
||||
|
||||
suspend fun joinConference(roomId: String, jitsiWidget: Widget, enableVideo: Boolean): JitsiCallViewEvents.JoinConference {
|
||||
val me = session.getRoomMember(session.myUserId, roomId)?.toMatrixItem()
|
||||
val userDisplayName = me?.getBestName()
|
||||
val userAvatar = me?.avatarUrl?.let { session.contentUrlResolver().resolveFullSize(it) }
|
||||
val userInfo = JitsiMeetUserInfo().apply {
|
||||
this.displayName = userDisplayName
|
||||
this.avatar = userAvatar?.let { URL(it) }
|
||||
}
|
||||
val roomName = session.getRoomSummary(roomId)?.displayName
|
||||
val properties = session.widgetService().getWidgetComputedUrl(jitsiWidget, themeProvider.isLightTheme())
|
||||
?.let { url -> jitsiWidgetPropertiesFactory.create(url) } ?: throw IllegalStateException()
|
||||
|
||||
val token = if (jitsiWidget.isOpenIdJWTAuthenticationRequired()) {
|
||||
getOpenIdJWTToken(roomId, properties.domain, userDisplayName ?: session.myUserId, userAvatar ?: "")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return JitsiCallViewEvents.JoinConference(
|
||||
enableVideo = enableVideo,
|
||||
jitsiUrl = properties.domain.ensureProtocol(),
|
||||
subject = roomName ?: "",
|
||||
confId = properties.confId ?: "",
|
||||
userInfo = userInfo,
|
||||
token = token
|
||||
)
|
||||
}
|
||||
|
||||
private fun Widget.isOpenIdJWTAuthenticationRequired(): Boolean {
|
||||
return widgetContent.data[JITSI_AUTH_KEY] == JITSI_OPEN_ID_TOKEN_JWT_AUTH
|
||||
}
|
||||
|
||||
private suspend fun getOpenIdJWTToken(roomId: String, domain: String, userDisplayName: String, userAvatar: String): String {
|
||||
val openIdToken = session.openIdService().getOpenIdToken()
|
||||
return jitsiJWTFactory.create(
|
||||
openIdToken = openIdToken,
|
||||
jitsiServerDomain = domain,
|
||||
roomId = roomId,
|
||||
userAvatarUrl = userAvatar,
|
||||
userDisplayName = userDisplayName
|
||||
)
|
||||
}
|
||||
|
||||
private fun createConferenceId(roomId: String, jitsiAuth: String?): String {
|
||||
return if (jitsiAuth == JITSI_OPEN_ID_TOKEN_JWT_AUTH) {
|
||||
// Create conference ID from room ID
|
||||
// For compatibility with Jitsi, use base32 without padding.
|
||||
// More details here:
|
||||
// https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
|
||||
roomId.toBase32String(padding = false)
|
||||
} else {
|
||||
// Create a random enough jitsi conference id
|
||||
// Note: the jitsi server automatically creates conference when the conference
|
||||
// id does not exist yet
|
||||
var widgetSessionId = UUID.randomUUID().toString()
|
||||
if (widgetSessionId.length > 8) {
|
||||
widgetSessionId = widgetSessionId.substring(0, 7)
|
||||
}
|
||||
roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.lowercase(VectorLocale.applicationLocale)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getJitsiAuth(jitsiDomain: String): String? {
|
||||
val request = Request.Builder().url("$jitsiDomain/.well-known/element/jitsi".ensureProtocol()).build()
|
||||
return tryOrNull {
|
||||
val response = session.getOkHttpClient().newCall(request).await()
|
||||
val json = response.body?.string() ?: return null
|
||||
MoshiProvider.providesMoshi().adapter(JitsiWellKnown::class.java).fromJson(json)?.auth
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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.conference
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class JitsiWellKnown(
|
||||
@Json(name = "auth") val auth: String
|
||||
)
|
|
@ -25,6 +25,7 @@ import android.content.res.Configuration
|
|||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
|
@ -86,8 +87,9 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
|
|||
|
||||
jitsiViewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is JitsiCallViewEvents.StartConference -> configureJitsiView(it)
|
||||
is JitsiCallViewEvents.JoinConference -> configureJitsiView(it)
|
||||
is JitsiCallViewEvents.ConfirmSwitchingConference -> handleConfirmSwitching(it)
|
||||
JitsiCallViewEvents.FailJoiningConference -> handleFailJoining()
|
||||
JitsiCallViewEvents.Finish -> finish()
|
||||
JitsiCallViewEvents.LeaveConference -> handleLeaveConference()
|
||||
}.exhaustive
|
||||
|
@ -138,12 +140,18 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
|
|||
}
|
||||
}
|
||||
|
||||
private fun configureJitsiView(startConference: JitsiCallViewEvents.StartConference) {
|
||||
private fun handleFailJoining() {
|
||||
Toast.makeText(this, getString(R.string.error_jitsi_join_conf), Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun configureJitsiView(joinConference: JitsiCallViewEvents.JoinConference) {
|
||||
val jitsiMeetConferenceOptions = JitsiMeetConferenceOptions.Builder()
|
||||
.setVideoMuted(!startConference.enableVideo)
|
||||
.setUserInfo(startConference.userInfo)
|
||||
.setVideoMuted(!joinConference.enableVideo)
|
||||
.setUserInfo(joinConference.userInfo)
|
||||
.setToken(joinConference.token)
|
||||
.apply {
|
||||
tryOrNull { URL(startConference.jitsiUrl) }?.let {
|
||||
tryOrNull { URL(joinConference.jitsiUrl) }?.let {
|
||||
setServerURL(it)
|
||||
}
|
||||
}
|
||||
|
@ -153,8 +161,8 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
|
|||
.setFeatureFlag("add-people.enabled", false)
|
||||
.setFeatureFlag("video-share.enabled", false)
|
||||
.setFeatureFlag("call-integration.enabled", false)
|
||||
.setRoom(startConference.confId)
|
||||
.setSubject(startConference.subject)
|
||||
.setRoom(joinConference.confId)
|
||||
.setSubject(joinConference.subject)
|
||||
.build()
|
||||
jitsiMeetView?.join(jitsiMeetConferenceOptions)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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.conference.jwt
|
||||
|
||||
import im.vector.app.core.utils.ensureProtocol
|
||||
import io.jsonwebtoken.Jwts
|
||||
import io.jsonwebtoken.SignatureAlgorithm
|
||||
import io.jsonwebtoken.security.Keys
|
||||
import org.matrix.android.sdk.api.session.openid.OpenIdToken
|
||||
import javax.inject.Inject
|
||||
|
||||
class JitsiJWTFactory @Inject constructor() {
|
||||
|
||||
/**
|
||||
* Create a JWT token for jitsi openidtoken-jwt authentication
|
||||
* See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
|
||||
*/
|
||||
fun create(openIdToken: OpenIdToken,
|
||||
jitsiServerDomain: String,
|
||||
roomId: String,
|
||||
userAvatarUrl: String,
|
||||
userDisplayName: String): String {
|
||||
// The secret key here is irrelevant, we're only using the JWT to transport data to Prosody in the Jitsi stack.
|
||||
val key = Keys.secretKeyFor(SignatureAlgorithm.HS256)
|
||||
val context = mapOf(
|
||||
"matrix" to mapOf(
|
||||
"token" to openIdToken.accessToken,
|
||||
"room_id" to roomId,
|
||||
"server_name" to openIdToken.matrixServerName
|
||||
),
|
||||
"user" to mapOf(
|
||||
"name" to userDisplayName,
|
||||
"avatar" to userAvatarUrl
|
||||
)
|
||||
)
|
||||
// As per Jitsi token auth, `iss` needs to be set to something agreed between
|
||||
// JWT generating side and Prosody config. Since we have no configuration for
|
||||
// the widgets, we can't set one anywhere. Using the Jitsi domain here probably makes sense.
|
||||
return Jwts.builder()
|
||||
.setIssuer(jitsiServerDomain)
|
||||
.setSubject(jitsiServerDomain)
|
||||
.setAudience(jitsiServerDomain.ensureProtocol())
|
||||
// room is not used at the moment, a * works here.
|
||||
.claim("room", "*")
|
||||
.claim("context", context)
|
||||
.signWith(key)
|
||||
.compact()
|
||||
}
|
||||
}
|
|
@ -38,6 +38,7 @@ import im.vector.app.core.extensions.exhaustive
|
|||
import im.vector.app.core.mvrx.runCatchingToAsync
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.call.conference.JitsiService
|
||||
import im.vector.app.features.call.dialpad.DialPadLookup
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import im.vector.app.features.command.CommandParser
|
||||
|
@ -52,9 +53,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.TimelineSettingsF
|
|||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||
import im.vector.app.features.home.room.typing.TypingHelper
|
||||
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
|
||||
import im.vector.app.features.raw.wellknown.getElementWellknown
|
||||
import im.vector.app.features.session.coroutineScope
|
||||
import im.vector.app.features.settings.VectorLocale
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
|
@ -68,7 +67,6 @@ import org.matrix.android.sdk.api.MatrixCallback
|
|||
import org.matrix.android.sdk.api.MatrixPatterns
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.raw.RawService
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.call.PSTNProtocolChecker
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
|
@ -99,13 +97,11 @@ import org.matrix.android.sdk.api.session.room.timeline.getRelationContent
|
|||
import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent
|
||||
import org.matrix.android.sdk.api.session.space.CreateSpaceParams
|
||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||
import org.matrix.android.sdk.api.util.appendParamToUrl
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
||||
import org.matrix.android.sdk.rx.rx
|
||||
import org.matrix.android.sdk.rx.unwrap
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
|
@ -115,7 +111,6 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
private val stringProvider: StringProvider,
|
||||
private val rainbowGenerator: RainbowGenerator,
|
||||
private val session: Session,
|
||||
private val rawService: RawService,
|
||||
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider,
|
||||
private val stickerPickerActionHandler: StickerPickerActionHandler,
|
||||
private val roomSummariesHolder: RoomSummariesHolder,
|
||||
|
@ -123,6 +118,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
private val callManager: WebRtcCallManager,
|
||||
private val chatEffectManager: ChatEffectManager,
|
||||
private val directRoomHelper: DirectRoomHelper,
|
||||
private val jitsiService: JitsiService,
|
||||
timelineSettingsFactory: TimelineSettingsFactory
|
||||
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
|
||||
Timeline.Listener, ChatEffectManager.Delegate, PSTNProtocolChecker.Listener {
|
||||
|
@ -186,7 +182,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
tryOrNull { room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) }
|
||||
}
|
||||
// Inform the SDK that the room is displayed
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
tryOrNull { session.onRoomDisplayed(initialState.roomId) }
|
||||
}
|
||||
callManager.addPstnSupportListener(this)
|
||||
|
@ -267,67 +263,67 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
|
||||
override fun handle(action: RoomDetailAction) {
|
||||
when (action) {
|
||||
is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action)
|
||||
is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action)
|
||||
is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
|
||||
is RoomDetailAction.SendMessage -> handleSendMessage(action)
|
||||
is RoomDetailAction.SendMedia -> handleSendMedia(action)
|
||||
is RoomDetailAction.SendSticker -> handleSendSticker(action)
|
||||
is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action)
|
||||
is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action)
|
||||
is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action)
|
||||
is RoomDetailAction.SendReaction -> handleSendReaction(action)
|
||||
is RoomDetailAction.AcceptInvite -> handleAcceptInvite()
|
||||
is RoomDetailAction.RejectInvite -> handleRejectInvite()
|
||||
is RoomDetailAction.RedactAction -> handleRedactEvent(action)
|
||||
is RoomDetailAction.UndoReaction -> handleUndoReact(action)
|
||||
is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
|
||||
is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action)
|
||||
is RoomDetailAction.EnterEditMode -> handleEditAction(action)
|
||||
is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action)
|
||||
is RoomDetailAction.EnterReplyMode -> handleReplyAction(action)
|
||||
is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action)
|
||||
is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
|
||||
is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action)
|
||||
is RoomDetailAction.ResendMessage -> handleResendEvent(action)
|
||||
is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
|
||||
is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
|
||||
is RoomDetailAction.ReportContent -> handleReportContent(action)
|
||||
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
|
||||
is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action)
|
||||
is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action)
|
||||
is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
|
||||
is RoomDetailAction.SendMessage -> handleSendMessage(action)
|
||||
is RoomDetailAction.SendMedia -> handleSendMedia(action)
|
||||
is RoomDetailAction.SendSticker -> handleSendSticker(action)
|
||||
is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action)
|
||||
is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action)
|
||||
is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action)
|
||||
is RoomDetailAction.SendReaction -> handleSendReaction(action)
|
||||
is RoomDetailAction.AcceptInvite -> handleAcceptInvite()
|
||||
is RoomDetailAction.RejectInvite -> handleRejectInvite()
|
||||
is RoomDetailAction.RedactAction -> handleRedactEvent(action)
|
||||
is RoomDetailAction.UndoReaction -> handleUndoReact(action)
|
||||
is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
|
||||
is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action)
|
||||
is RoomDetailAction.EnterEditMode -> handleEditAction(action)
|
||||
is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action)
|
||||
is RoomDetailAction.EnterReplyMode -> handleReplyAction(action)
|
||||
is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action)
|
||||
is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
|
||||
is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action)
|
||||
is RoomDetailAction.ResendMessage -> handleResendEvent(action)
|
||||
is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
|
||||
is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
|
||||
is RoomDetailAction.ReportContent -> handleReportContent(action)
|
||||
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
|
||||
is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
|
||||
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
|
||||
is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action)
|
||||
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
|
||||
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
|
||||
is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
|
||||
is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action)
|
||||
is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action)
|
||||
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()
|
||||
is RoomDetailAction.ManageIntegrations -> handleManageIntegrations()
|
||||
is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action)
|
||||
is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId)
|
||||
is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action)
|
||||
is RoomDetailAction.CancelSend -> handleCancel(action)
|
||||
is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action)
|
||||
is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action)
|
||||
RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople()
|
||||
RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar()
|
||||
is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action)
|
||||
RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings)
|
||||
is RoomDetailAction.ShowRoomAvatarFullScreen -> {
|
||||
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
|
||||
is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action)
|
||||
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
|
||||
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
|
||||
is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
|
||||
is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action)
|
||||
is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action)
|
||||
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()
|
||||
is RoomDetailAction.ManageIntegrations -> handleManageIntegrations()
|
||||
is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action)
|
||||
is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId)
|
||||
is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action)
|
||||
is RoomDetailAction.CancelSend -> handleCancel(action)
|
||||
is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action)
|
||||
is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action)
|
||||
RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople()
|
||||
RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar()
|
||||
is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action)
|
||||
RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings)
|
||||
is RoomDetailAction.ShowRoomAvatarFullScreen -> {
|
||||
_viewEvents.post(
|
||||
RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView)
|
||||
)
|
||||
}
|
||||
is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action)
|
||||
RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages()
|
||||
RoomDetailAction.ResendAll -> handleResendAll()
|
||||
is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action)
|
||||
RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages()
|
||||
RoomDetailAction.ResendAll -> handleResendAll()
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
|
@ -437,57 +433,8 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
private fun handleAddJitsiConference(action: RoomDetailAction.AddJitsiWidget) {
|
||||
_viewEvents.post(RoomDetailViewEvents.ShowWaitingView)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// Build data for a jitsi widget
|
||||
val widgetId: String = WidgetType.Jitsi.preferred + "_" + session.myUserId + "_" + System.currentTimeMillis()
|
||||
|
||||
// Create a random enough jitsi conference id
|
||||
// Note: the jitsi server automatically creates conference when the conference
|
||||
// id does not exist yet
|
||||
var widgetSessionId = UUID.randomUUID().toString()
|
||||
|
||||
if (widgetSessionId.length > 8) {
|
||||
widgetSessionId = widgetSessionId.substring(0, 7)
|
||||
}
|
||||
val roomId: String = room.roomId
|
||||
val confId = roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.lowercase(VectorLocale.applicationLocale)
|
||||
|
||||
val preferredJitsiDomain = tryOrNull {
|
||||
rawService.getElementWellknown(session.myUserId)
|
||||
?.jitsiServer
|
||||
?.preferredDomain
|
||||
}
|
||||
val jitsiDomain = preferredJitsiDomain ?: stringProvider.getString(R.string.preferred_jitsi_domain)
|
||||
|
||||
// We use the default element wrapper for this widget
|
||||
// https://github.com/vector-im/element-web/blob/develop/docs/jitsi-dev.md
|
||||
// https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/utils/WidgetUtils.ts#L469
|
||||
val url = buildString {
|
||||
append("https://app.element.io/jitsi.html")
|
||||
appendParamToUrl("confId", confId)
|
||||
append("#conferenceDomain=\$domain")
|
||||
append("&conferenceId=\$conferenceId")
|
||||
append("&isAudioOnly=\$isAudioOnly")
|
||||
append("&displayName=\$matrix_display_name")
|
||||
append("&avatarUrl=\$matrix_avatar_url")
|
||||
append("&userId=\$matrix_user_id")
|
||||
append("&roomId=\$matrix_room_id")
|
||||
append("&theme=\$theme")
|
||||
}
|
||||
val widgetEventContent = mapOf(
|
||||
"url" to url,
|
||||
"type" to WidgetType.Jitsi.legacy,
|
||||
"data" to mapOf(
|
||||
"conferenceId" to confId,
|
||||
"domain" to jitsiDomain,
|
||||
"isAudioOnly" to !action.withVideo
|
||||
),
|
||||
"creatorUserId" to session.myUserId,
|
||||
"id" to widgetId,
|
||||
"name" to "jitsi"
|
||||
)
|
||||
|
||||
try {
|
||||
val widget = session.widgetService().createRoomWidget(roomId, widgetId, widgetEventContent)
|
||||
val widget = jitsiService.createJitsiWidget(room.roomId, action.withVideo)
|
||||
_viewEvents.post(RoomDetailViewEvents.JoinJitsiConference(widget, action.withVideo))
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.failed_to_add_widget)))
|
||||
|
@ -670,13 +617,13 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
}
|
||||
when (itemId) {
|
||||
R.id.timeline_setting -> true
|
||||
R.id.invite -> state.canInvite
|
||||
R.id.invite -> state.canInvite
|
||||
R.id.open_matrix_apps -> true
|
||||
R.id.voice_call,
|
||||
R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty()
|
||||
R.id.hangup_call -> callManager.getCallsByRoomId(state.roomId).isNotEmpty()
|
||||
R.id.search -> true
|
||||
R.id.dev_tools -> vectorPreferences.developerMode()
|
||||
R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty()
|
||||
R.id.hangup_call -> callManager.getCallsByRoomId(state.roomId).isNotEmpty()
|
||||
R.id.search -> true
|
||||
R.id.dev_tools -> vectorPreferences.developerMode()
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1675,6 +1675,7 @@
|
|||
|
||||
|
||||
<string name="error_jitsi_not_supported_on_old_device">Sorry, conference calls with Jitsi are not supported on old devices (devices with Android OS below 6.0)</string>
|
||||
<string name="error_jitsi_join_conf">Sorry, an error occurred while trying to join the conference</string>
|
||||
<string name="jitsi_leave_conf_to_join_another_one_content">Leave the current conference and switch to the other one?</string>
|
||||
|
||||
<string name="room_widget_resource_permission_title">This widget wants to use the following resources:</string>
|
||||
|
|
Loading…
Reference in a new issue