Merge pull request #1914 from vector-im/feature/jitsi_native

Feature/jitsi native
This commit is contained in:
Benoit Marty 2020-08-17 10:54:31 +02:00 committed by GitHub
commit 550dcde9b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1480 additions and 158 deletions

View file

@ -14,6 +14,7 @@
<w>gplay</w>
<w>hmac</w>
<w>homeserver</w>
<w>jitsi</w>
<w>ktlint</w>
<w>linkified</w>
<w>linkify</w>

View file

@ -3,6 +3,7 @@ Changes in Element 1.0.5 (2020-XX-XX)
Features ✨:
- Protect access to the app by a pin code (#1700)
- Conference with Jitsi support (#43)
Improvements 🙌:
- Give user the possibility to prevent accidental call (#1869)

View file

@ -52,6 +52,12 @@ allprojects {
}
}
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
// Jitsi repo
maven {
url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-2.9.3"
// Note: to test Jitsi release you can use a local file like this:
// url "file:///Users/bmarty/workspaces/jitsi_libre_maven/android-sdk-2.9.3"
}
google()
jcenter()
}

82
docs/jitsi.md Normal file
View file

@ -0,0 +1,82 @@
# Jitsi in Element Android
Native Jitsi support has been added to Element Android by the PR [#1914](https://github.com/vector-im/element-android/pull/1914). The description of the PR contains some documentation about the behaviour in each possible room configuration.
Also, ensure to have a look on [the documentation from Element Web](https://github.com/vector-im/element-web/blob/develop/docs/jitsi.md)
The official documentation about how to integrate the Jitsi SDK in an Android app is available here: https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-android-sdk.
# Native Jitsi SDK
The Jitsi SDK is built by ourselves with the flag LIBRE_BUILD, to be able to be integrated on the F-Droid version of Element Android.
The generated maven repository is then host in the project https://github.com/vector-im/jitsi_libre_maven
## How to build the Jitsi Meet SDK
### Jitsi version
Update the script `./tools/jitsi/build_jisti_libs.sh` with the tag of the project `https://github.com/jitsi/jitsi-meet`.
Currently we are building the version with the tag `android-sdk-2.9.3`.
### Run the build script
At the root of the Element Android, run the following script:
```shell script
./tools/jitsi/build_jisti_libs.sh
```
It will build the Jitsi Meet Android library and put every generated files in the folder `/tmp/jitsi`
### Link with the new generated library
- Update the file `./build.gradle` to use the previously created local Maven repository. Currently we have this line:
```groovy
url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-2.9.3"
```
You can uncomment and update the line starting with `// url "file://...` and comment the line starting with `url`, to test the library using the locally generated Maven repository.
- Update the dependency of the WebRTC library in the file `./matrix-sdk-android/build.gradle`. Currently we have this line:
```groovy
implementation('com.facebook.react:react-native-webrtc:1.84.0-jitsi-5112273@aar')
```
- Update the dependency of the Jitsi Meet library in the file `./vector/build.gradle`. Currently we have this line:
```groovy
implementation('org.jitsi.react:jitsi-meet-sdk:2.9.3') { transitive = true }
```
- Perform a gradle sync and build the project
- Perform test
### Sanity tests
In order to validate that the upgrade of the Jitsi and WebRTC dependency does not break anything, the following sanity tests have to be performed, using two devices:
- Make 1-1 audio call (so using WebRTC)
- Make 1-1 video call (so using WebRTC)
- Create and join a conference call with audio only (so using Jitsi library). Leave the conference. Join it again.
- Create and join a conference call with audio and video (so using Jitsi library) Leave the conference. Join it again.
### Export the build library
If all the tests are passed, you can export the generated Jitsi library to our Maven repository.
- Clone the project https://github.com/vector-im/jitsi_libre_maven.
- Create a new folder with the version name.
- Copy every generated files form `/tmp/jitsi` to the folder you have just created.
- Commit and push the change on https://github.com/vector-im/jitsi_libre_maven.
- Update the file `./build.gradle` to use the previously created Maven repository. Currently we have this line:
```groovy
url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-2.9.3"
```
- Build the project and perform the sanity tests again.
- Update the file `/CANGES.md` to notify about the library upgrade, and create a regular PR for project Element Android.

View file

@ -132,8 +132,13 @@ dependencies {
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version"
implementation 'com.squareup.okhttp3:okhttp:4.2.2'
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.8.1"))
implementation 'com.squareup.okhttp3:okhttp'
implementation 'com.squareup.okhttp3:logging-interceptor'
implementation 'com.squareup.okhttp3:okhttp-urlconnection'
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
@ -174,8 +179,10 @@ dependencies {
implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23'
// Web RTC
// TODO meant for development purposes only. See http://webrtc.github.io/webrtc-org/native-code/android/
implementation 'org.webrtc:google-webrtc:1.0.+'
// org.webrtc:google-webrtc is for development purposes only. See http://webrtc.github.io/webrtc-org/native-code/android/
// implementation 'org.webrtc:google-webrtc:1.0.+'
// Use the same WebRTC library than the one used by Jitsi library
implementation('com.facebook.react:react-native-webrtc:1.84.0-jitsi-5112273@aar')
debugImplementation 'com.airbnb.okreplay:okreplay:1.5.0'
releaseImplementation 'com.airbnb.okreplay:noop:1.5.0'

View file

@ -42,6 +42,9 @@ import org.matrix.android.sdk.api.util.JsonDict
* }
* ]
* }
* "im.vector.riot.jitsi": {
* "preferredDomain": "https://jitsi.riot.im/"
* }
* }
* </pre>
*/
@ -57,7 +60,10 @@ data class WellKnown(
val integrations: JsonDict? = null,
@Json(name = "im.vector.riot.e2ee")
val e2eAdminSetting: E2EWellKnownConfig? = null
val e2eAdminSetting: E2EWellKnownConfig? = null,
@Json(name = "im.vector.riot.jitsi")
val jitsiServer: WellKnownPreferredConfig? = null
)
@ -66,3 +72,9 @@ data class E2EWellKnownConfig(
@Json(name = "default")
val e2eDefault: Boolean = true
)
@JsonClass(generateAdapter = true)
data class WellKnownPreferredConfig(
@Json(name = "preferredDomain")
val preferredDomain: String? = null
)

View file

@ -38,7 +38,11 @@ data class HomeServerCapabilities(
* Option to allow homeserver admins to set the default E2EE behaviour back to disabled for DMs / private rooms
* (as it was before) for various environments where this is desired.
*/
val adminE2EByDefault: Boolean = true
val adminE2EByDefault: Boolean = true,
/**
* Preferred Jitsi domain, provided in Wellknown
*/
val preferredJitsiDomain: String? = null
) {
companion object {
const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L

View file

@ -17,10 +17,10 @@
package org.matrix.android.sdk.internal.database
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import io.realm.DynamicRealm
import io.realm.RealmMigration
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import timber.log.Timber
import javax.inject.Inject
@ -31,6 +31,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
if (oldVersion <= 0) migrateTo1(realm)
if (oldVersion <= 1) migrateTo2(realm)
if (oldVersion <= 2) migrateTo3(realm)
}
private fun migrateTo1(realm: DynamicRealm) {
@ -52,4 +53,14 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
obj.setBoolean(HomeServerCapabilitiesEntityFields.ADMIN_E2_E_BY_DEFAULT, true)
}
}
private fun migrateTo3(realm: DynamicRealm) {
Timber.d("Step 2 -> 3")
realm.schema.get("HomeServerCapabilitiesEntity")
?.addField(HomeServerCapabilitiesEntityFields.PREFERRED_JITSI_DOMAIN, String::class.java)
?.transform { obj ->
// Schedule a refresh of the capabilities
obj.setLong(HomeServerCapabilitiesEntityFields.LAST_UPDATED_TIMESTAMP, 0)
}
}
}

View file

@ -47,7 +47,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(
context: Context) {
companion object {
const val SESSION_STORE_SCHEMA_VERSION = 2L
const val SESSION_STORE_SCHEMA_VERSION = 3L
}
// Keep legacy preferences name for compatibility reason

View file

@ -31,7 +31,8 @@ internal object HomeServerCapabilitiesMapper {
maxUploadFileSize = entity.maxUploadFileSize,
lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported,
defaultIdentityServerUrl = entity.defaultIdentityServerUrl,
adminE2EByDefault = entity.adminE2EByDefault
adminE2EByDefault = entity.adminE2EByDefault,
preferredJitsiDomain = entity.preferredJitsiDomain
)
}
}

View file

@ -26,7 +26,8 @@ internal open class HomeServerCapabilitiesEntity(
var lastVersionIdentityServerSupported: Boolean = false,
var defaultIdentityServerUrl: String? = null,
var adminE2EByDefault: Boolean = true,
var lastUpdatedTimestamp: Long = 0L
var lastUpdatedTimestamp: Long = 0L,
var preferredJitsiDomain: String? = null
) : RealmObject() {
companion object

View file

@ -77,7 +77,11 @@ internal object NetworkModule {
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.addNetworkInterceptor(stethoInterceptor)
.apply {
if (BuildConfig.DEBUG) {
addNetworkInterceptor(stethoInterceptor)
}
}
.addInterceptor(timeoutInterceptor)
.addInterceptor(userAgentInterceptor)
.addInterceptor(httpLoggingInterceptor)

View file

@ -110,6 +110,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) {
homeServerCapabilitiesEntity.defaultIdentityServerUrl = getWellknownResult.identityServerUrl
homeServerCapabilitiesEntity.adminE2EByDefault = getWellknownResult.wellKnown.e2eAdminSetting?.e2eDefault ?: true
homeServerCapabilitiesEntity.preferredJitsiDomain = getWellknownResult.wellKnown.jitsiServer?.preferredDomain
// We are also checking for integration manager configurations
val config = configExtractor.extract(getWellknownResult.wellKnown)
if (config != null) {

65
tools/jitsi/build_jisti_libs.sh Executable file
View file

@ -0,0 +1,65 @@
#!/usr/bin/env bash
########
# This script build the Jitsi library with LIBRE_BUILD flag.
# Following instructions from here https://github.com/jitsi/jitsi-meet/tree/master/android#build-and-use-your-own-sdk-artifactsbinaries
# It then export the library in a maven repository, that we host here https://github.com/vector-im/jitsi_libre_maven
# exit on any error
set -e
echo
echo "##################################################"
echo "Cloning jitsi-meet repository"
echo "##################################################"
cd ..
rm -rf jitsi-meet
git clone https://github.com/jitsi/jitsi-meet
# We want a libre build!
export LIBRE_BUILD=true
cd jitsi-meet
# This is commit after version 2.2.2, which does not compile
# git checkout 5a934c071a5cbe64de275a25d0ed62d8193cdd03
# Version android-sdk-2.9.3, commit abcbbbea12e3ef88012b14723bb8cd42dbefc988
git checkout android-sdk-2.9.3
echo
echo "##################################################"
echo "npm install"
echo "##################################################"
npm install
#make
#echo
#echo "##################################################"
#echo "Build the Android library"
#echo "##################################################"
#
#pushd android
#./gradlew assembleRelease
#popd
#
#echo
#echo "##################################################"
#echo "Bundle with React Native"
#echo "##################################################"
#
#react-native bundle --platform android --dev false --entry-file index.android.js --bundle-output index.android.bundle --assets-dest android/app/src/main/res/
./android/scripts/release-sdk.sh /tmp/jitsi/
# Also copy jsc
mkdir -p /tmp/jitsi/org/webkit/
cp -r ./node_modules/jsc-android/dist/org/webkit/android-jsc /tmp/jitsi/org/webkit/
echo
echo "##################################################"
echo "Release has been done here: /tmp/jitsi/"
echo "##################################################"

View file

@ -405,8 +405,10 @@ dependencies {
implementation 'com.github.BillCarsonFr:JsonViewer:0.5'
// TODO meant for development purposes only
implementation 'org.webrtc:google-webrtc:1.0.+'
// WebRTC
// org.webrtc:google-webrtc is for development purposes only
// implementation 'org.webrtc:google-webrtc:1.0.+'
implementation('org.jitsi.react:jitsi-meet-sdk:2.9.3') { transitive = true }
// QR-code
// Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170

View file

@ -40,9 +40,11 @@
<issue id="Recycle" severity="error" />
<issue id="KotlinPropertyAccess" severity="error" />
<!-- Ignore error from HtmlCompressor lib -->
<issue id="InvalidPackage">
<!-- Ignore error from HtmlCompressor lib -->
<ignore path="**/htmlcompressor-1.4.jar" />
<!-- Ignore error from dropbox-core-sdk-3.0.8 lib, which comes with Jitsi library -->
<ignore path="**/dropbox-core-sdk-3.0.8.jar" />
</issue>
<!-- Manifest -->

View file

@ -24,3 +24,44 @@
## print all the rules in a file
# -printconfiguration ../proguard_files/full-r8-config.txt
# WebRTC
-keep class org.webrtc.** { *; }
-dontwarn org.chromium.build.BuildHooksAndroid
# Jitsi (else callbacks are not called)
-keep class org.jitsi.meet.** { *; }
-keep class org.jitsi.meet.sdk.** { *; }
# React Native
# Keep our interfaces so they can be used by other ProGuard rules.
# See http://sourceforge.net/p/proguard/bugs/466/
-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip
-keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters
-keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip
# Do not strip any method/class that is annotated with @DoNotStrip
-keep @com.facebook.proguard.annotations.DoNotStrip class *
-keep @com.facebook.common.internal.DoNotStrip class *
-keepclassmembers class * {
@com.facebook.proguard.annotations.DoNotStrip *;
@com.facebook.common.internal.DoNotStrip *;
}
-keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * {
void set*(***);
*** get*();
}
-keep class * extends com.facebook.react.bridge.JavaScriptModule { *; }
-keep class * extends com.facebook.react.bridge.NativeModule { *; }
-keepclassmembers,includedescriptorclasses class * { native <methods>; }
-keepclassmembers class * { @com.facebook.react.uimanager.UIProp <fields>; }
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp <methods>; }
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup <methods>; }
-dontwarn com.facebook.react.**
-keep,includedescriptorclasses class com.facebook.react.bridge.** { *; }

View file

@ -28,6 +28,14 @@
<!-- Needed for incoming calls -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- Jitsi libs adds CALENDAR permissions, but we can remove them safely according to https://github.com/jitsi/jitsi-meet/issues/4068#issuecomment-480482481 -->
<uses-permission
android:name="android.permission.READ_CALENDAR"
tools:node="remove" />
<uses-permission
android:name="android.permission.WRITE_CALENDAR"
tools:node="remove" />
<!-- Adding CAMERA permission prevents Chromebooks to see the application on the PlayStore -->
<!-- Tell that the Camera is not mandatory to install the application -->
<uses-feature
@ -202,6 +210,9 @@
android:name="im.vector.app.features.attachments.preview.AttachmentsPreviewActivity"
android:theme="@style/AppTheme.AttachmentsPreview" />
<activity android:name="im.vector.app.features.call.VectorCallActivity" />
<activity
android:name="im.vector.app.features.call.conference.VectorJitsiActivity"
android:configChanges="orientation|screenSize" />
<activity android:name="im.vector.app.features.terms.ReviewTermsActivity" />
<activity android:name="im.vector.app.features.widgets.WidgetActivity" />

View file

@ -21,9 +21,6 @@
div {
padding: 4px;
}
</style>
</head>
<body>
@ -392,6 +389,13 @@ SOFTWARE.
<li>
<b>Copyright (C) 2018 stfalcon.com</b>
</li>
<li>
<b>Jitsi Meet (<a href="https://github.com/jitsi/jitsi-meet">jitsi-meet</a>)</b>
<br/>
Copyright @ 2020-present 8x8, Inc.
<br/>
Copyright @ 2017-2018 Atlassian Pty Ltd
</li>
</ul>
<pre>
Apache License

View file

@ -27,6 +27,7 @@ import im.vector.app.core.preference.UserAvatarPreference
import im.vector.app.features.MainActivity
import im.vector.app.features.call.CallControlsBottomSheet
import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.conference.VectorJitsiActivity
import im.vector.app.features.createdirect.CreateDirectRoomActivity
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
@ -140,6 +141,7 @@ interface ScreenComponent {
fun inject(activity: WidgetActivity)
fun inject(activity: VectorCallActivity)
fun inject(activity: VectorAttachmentViewerActivity)
fun inject(activity: VectorJitsiActivity)
/* ==========================================================================================
* BottomSheets

View file

@ -36,7 +36,7 @@ abstract class GenericButtonItem : VectorEpoxyModel<GenericButtonItem.Holder>()
var text: String? = null
@EpoxyAttribute
var itemClickAction: View.OnClickListener? = null
var buttonClickAction: View.OnClickListener? = null
@EpoxyAttribute
@ColorInt
@ -57,7 +57,7 @@ abstract class GenericButtonItem : VectorEpoxyModel<GenericButtonItem.Holder>()
holder.button.icon = null
}
itemClickAction?.let { holder.view.setOnClickListener(it) }
buttonClickAction?.let { holder.button.setOnClickListener(it) }
}
class Holder : VectorEpoxyHolder() {

View file

@ -0,0 +1,113 @@
/*
* 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.core.ui.views
import android.content.Context
import android.text.SpannableString
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.util.AttributeSet
import android.view.View
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.core.view.isVisible
import im.vector.app.R
import im.vector.app.core.utils.tappableMatchingText
import im.vector.app.features.home.room.detail.RoomDetailViewState
import im.vector.app.features.themes.ThemeUtils
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
class ActiveConferenceView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) {
interface Callback {
fun onTapJoinAudio(jitsiWidget: Widget)
fun onTapJoinVideo(jitsiWidget: Widget)
fun onDelete(jitsiWidget: Widget)
}
var callback: Callback? = null
var jitsiWidget: Widget? = null
init {
setupView()
}
private fun setupView() {
inflate(context, R.layout.view_active_conference_view, this)
setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorPrimary))
// "voice" and "video" texts are underlined and clickable
val voiceString = context.getString(R.string.ongoing_conference_call_voice)
val videoString = context.getString(R.string.ongoing_conference_call_video)
val fullMessage = context.getString(R.string.ongoing_conference_call, voiceString, videoString)
val styledText = SpannableString(fullMessage)
styledText.tappableMatchingText(voiceString, object : ClickableSpan() {
override fun onClick(widget: View) {
jitsiWidget?.let {
callback?.onTapJoinAudio(it)
}
}
})
styledText.tappableMatchingText(videoString, object : ClickableSpan() {
override fun onClick(widget: View) {
jitsiWidget?.let {
callback?.onTapJoinVideo(it)
}
}
})
findViewById<TextView>(R.id.activeConferenceInfo).apply {
text = styledText
movementMethod = LinkMovementMethod.getInstance()
}
findViewById<TextView>(R.id.deleteWidgetButton).setOnClickListener {
jitsiWidget?.let { callback?.onDelete(it) }
}
}
fun render(state: RoomDetailViewState) {
val summary = state.asyncRoomSummary()
if (summary?.membership == Membership.JOIN) {
// We only display banner for 'live' widgets
val activeConf =
state.activeRoomWidgets()?.firstOrNull {
// for now only jitsi?
it.type == WidgetType.Jitsi
}
if (activeConf == null) {
isVisible = false
} else {
isVisible = true
jitsiWidget = activeConf
}
// if sent by me or if i can moderate?
findViewById<TextView>(R.id.deleteWidgetButton).isVisible = state.isAllowedToManageWidgets
} else {
isVisible = false
}
}
}

View file

@ -0,0 +1,21 @@
/*
* 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.conference
import im.vector.app.core.platform.VectorViewModelAction
sealed class JitsiCallViewActions : VectorViewModelAction

View file

@ -0,0 +1,21 @@
/*
* 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.conference
import im.vector.app.core.platform.VectorViewEvents
sealed class JitsiCallViewEvents : VectorViewEvents

View file

@ -0,0 +1,112 @@
/*
* 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.conference
import android.net.Uri
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
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.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,
@Assisted val args: VectorJitsiActivity.Args,
private val session: Session,
private val stringProvider: StringProvider
) : VectorViewModel<JitsiCallViewState, JitsiCallViewActions, JitsiCallViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: JitsiCallViewState, args: VectorJitsiActivity.Args): JitsiCallViewModel
}
init {
val me = session.getUser(session.myUserId)?.toMatrixItem()
val userInfo = JitsiMeetUserInfo().apply {
displayName = me?.getBestName()
avatar = me?.avatarUrl?.let { session.contentUrlResolver().resolveFullSize(it) }?.let { URL(it) }
}
val roomName = session.getRoomSummary(args.roomId)?.displayName
setState {
copy(userInfo = userInfo)
}
session.widgetService().getRoomWidgetsLive(args.roomId, QueryStringValue.Equals(args.widgetId), WidgetType.Jitsi.values())
.asObservable()
.distinctUntilChanged()
.subscribe {
val jitsiWidget = it.firstOrNull()
if (jitsiWidget != null) {
val uri = Uri.parse(jitsiWidget.computedUrl)
val confId = uri.getQueryParameter("confId")
val ppt = jitsiWidget.computedUrl?.let { url -> JitsiWidgetProperties(url, stringProvider) }
setState {
copy(
widget = Success(jitsiWidget),
jitsiUrl = "https://${ppt?.domain}",
confId = confId ?: "",
subject = roomName ?: ""
)
}
} else {
setState {
copy(
widget = Fail(IllegalArgumentException("Widget not found"))
)
}
}
}
.disposeOnClear()
}
override fun handle(action: JitsiCallViewActions) {
}
companion object : MvRxViewModelFactory<JitsiCallViewModel, JitsiCallViewState> {
const val ENABLE_VIDEO_OPTION = "ENABLE_VIDEO_OPTION"
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: JitsiCallViewState): JitsiCallViewModel? {
val callActivity: VectorJitsiActivity = viewModelContext.activity()
val callArgs: VectorJitsiActivity.Args = viewModelContext.args()
return callActivity.viewModelFactory.create(state, callArgs)
}
override fun initialState(viewModelContext: ViewModelContext): JitsiCallViewState? {
val args: VectorJitsiActivity.Args = viewModelContext.args()
return JitsiCallViewState(
roomId = args.roomId,
widgetId = args.widgetId,
enableVideo = args.enableVideo
)
}
}
}

View file

@ -0,0 +1,34 @@
/*
* 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.conference
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import org.jitsi.meet.sdk.JitsiMeetUserInfo
import org.matrix.android.sdk.api.session.widgets.model.Widget
data class JitsiCallViewState(
val roomId: String = "",
val widgetId: String = "",
val enableVideo: Boolean = true,
val jitsiUrl: String = "",
val subject: String = "",
val confId: String = "",
val userInfo: JitsiMeetUserInfo = JitsiMeetUserInfo(),
val widget: Async<Widget> = Uninitialized
) : MvRxState

View file

@ -0,0 +1,37 @@
/*
* 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.conference
import android.net.Uri
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
class JitsiWidgetProperties(private val uriString: String, val stringProvider: StringProvider) {
val domain: String by lazy { configs["conferenceDomain"] ?: stringProvider.getString(R.string.preferred_jitsi_domain) }
val displayName: String? by lazy { configs["displayName"] }
val avatarUrl: String? by lazy { configs["avatarUrl"] }
private val configString: String? by lazy { Uri.parse(uriString).fragment }
private val configs: Map<String, String?> by lazy {
configString?.split("&")
?.map { it.split("=") }
?.map { (key, value) -> key to value }
?.toMap()
?: mapOf()
}
}

View file

@ -0,0 +1,174 @@
/*
* 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.conference
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import android.widget.FrameLayout
import androidx.core.view.isVisible
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.viewModel
import com.facebook.react.modules.core.PermissionListener
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.platform.VectorBaseActivity
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity_jitsi.*
import org.jitsi.meet.sdk.JitsiMeetActivityDelegate
import org.jitsi.meet.sdk.JitsiMeetActivityInterface
import org.jitsi.meet.sdk.JitsiMeetConferenceOptions
import org.jitsi.meet.sdk.JitsiMeetView
import org.jitsi.meet.sdk.JitsiMeetViewListener
import org.matrix.android.sdk.api.extensions.tryThis
import java.net.URL
import javax.inject.Inject
class VectorJitsiActivity : VectorBaseActivity(), JitsiMeetActivityInterface, JitsiMeetViewListener {
@Parcelize
data class Args(
val roomId: String,
val widgetId: String,
val enableVideo: Boolean
) : Parcelable
override fun getLayoutRes() = R.layout.activity_jitsi
@Inject lateinit var viewModelFactory: JitsiCallViewModel.Factory
private var jitsiMeetView: JitsiMeetView? = null
private val jitsiViewModel: JitsiCallViewModel by viewModel()
override fun injectWith(injector: ScreenComponent) {
super.injectWith(injector)
injector.inject(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
jitsiViewModel.subscribe(this) {
renderState(it)
}
}
override fun initUiAndData() {
super.initUiAndData()
jitsiMeetView = JitsiMeetView(this)
val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
jitsi_layout.addView(jitsiMeetView, params)
jitsiMeetView?.listener = this
}
private fun renderState(viewState: JitsiCallViewState) {
when (viewState.widget) {
is Fail -> finish()
is Success -> {
findViewById<View>(R.id.jitsi_progress_layout).isVisible = false
jitsiMeetView?.isVisible = true
configureJitsiView(viewState)
}
else -> {
jitsiMeetView?.isVisible = false
findViewById<View>(R.id.jitsi_progress_layout).isVisible = true
}
}
}
private fun configureJitsiView(viewState: JitsiCallViewState) {
val jitsiMeetConferenceOptions = JitsiMeetConferenceOptions.Builder()
.setVideoMuted(!viewState.enableVideo)
.setUserInfo(viewState.userInfo)
.apply {
tryThis { URL(viewState.jitsiUrl) }?.let {
setServerURL(it)
}
}
// https://github.com/jitsi/jitsi-meet/blob/master/react/features/base/flags/constants.js
.setFeatureFlag("chat.enabled", false)
.setFeatureFlag("invite.enabled", false)
.setFeatureFlag("add-people.enabled", false)
.setFeatureFlag("video-share.enabled", false)
.setRoom(viewState.confId)
.setSubject(viewState.subject)
.build()
jitsiMeetView?.join(jitsiMeetConferenceOptions)
}
override fun onPause() {
JitsiMeetActivityDelegate.onHostPause(this)
super.onPause()
}
override fun onResume() {
JitsiMeetActivityDelegate.onHostResume(this)
super.onResume()
}
override fun onBackPressed() {
JitsiMeetActivityDelegate.onBackPressed()
super.onBackPressed()
}
override fun onDestroy() {
JitsiMeetActivityDelegate.onHostDestroy(this)
super.onDestroy()
}
// override fun onUserLeaveHint() {
// super.onUserLeaveHint()
// jitsiMeetView?.enterPictureInPicture()
// }
override fun onNewIntent(intent: Intent?) {
JitsiMeetActivityDelegate.onNewIntent(intent)
super.onNewIntent(intent)
}
override fun requestPermissions(permissions: Array<out String>?, requestCode: Int, listener: PermissionListener?) {
JitsiMeetActivityDelegate.requestPermissions(this, permissions, requestCode, listener)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
JitsiMeetActivityDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onConferenceTerminated(p0: MutableMap<String, Any>?) {
finish()
}
override fun onConferenceJoined(p0: MutableMap<String, Any>?) {
}
override fun onConferenceWillJoin(p0: MutableMap<String, Any>?) {
}
companion object {
fun newIntent(context: Context, roomId: String, widgetId: String, enableVideo: Boolean): Intent {
return Intent(context, VectorJitsiActivity::class.java).apply {
putExtra(MvRx.KEY_ARG, Args(roomId, widgetId, enableVideo))
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
}
}
}

View file

@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageStickerConte
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.widgets.model.Widget
sealed class RoomDetailAction : VectorViewModelAction {
data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction()
@ -80,4 +81,10 @@ sealed class RoomDetailAction : VectorViewModelAction {
object SelectStickerAttachment : RoomDetailAction()
object OpenIntegrationManager: RoomDetailAction()
object ManageIntegrations: RoomDetailAction()
data class AddJitsiWidget(val withVideo: Boolean): RoomDetailAction()
data class RemoveWidget(val widgetId: String): RoomDetailAction()
data class EnsureNativeWidgetAllowed(val widget: Widget,
val userJustAccepted: Boolean,
val grantedEvents: RoomDetailViewEvents) : RoomDetailAction()
}

View file

@ -35,6 +35,8 @@ import im.vector.app.features.room.RequireActiveMembershipAction
import im.vector.app.features.room.RequireActiveMembershipViewEvents
import im.vector.app.features.room.RequireActiveMembershipViewModel
import im.vector.app.features.room.RequireActiveMembershipViewState
import im.vector.app.features.widgets.permissions.RoomWidgetPermissionViewModel
import im.vector.app.features.widgets.permissions.RoomWidgetPermissionViewState
import kotlinx.android.synthetic.main.activity_room_detail.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import javax.inject.Inject
@ -42,7 +44,8 @@ import javax.inject.Inject
class RoomDetailActivity :
VectorBaseActivity(),
ToolbarConfigurable,
RequireActiveMembershipViewModel.Factory {
RequireActiveMembershipViewModel.Factory,
RoomWidgetPermissionViewModel.Factory {
override fun getLayoutRes() = R.layout.activity_room_detail
@ -57,6 +60,12 @@ class RoomDetailActivity :
return requireActiveMembershipViewModelFactory.create(initialState.copy(roomId = currentRoomId ?: ""))
}
@Inject
lateinit var permissionsViewModelFactory: RoomWidgetPermissionViewModel.Factory
override fun create(initialState: RoomWidgetPermissionViewState): RoomWidgetPermissionViewModel {
return permissionsViewModelFactory.create(initialState)
}
override fun injectWith(injector: ScreenComponent) {
super.injectWith(injector)
injector.inject(this)

View file

@ -29,8 +29,11 @@ import android.text.Spannable
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
@ -77,6 +80,7 @@ import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.ui.views.ActiveCallView
import im.vector.app.core.ui.views.ActiveCallViewHolder
import im.vector.app.core.ui.views.ActiveConferenceView
import im.vector.app.core.ui.views.JumpToReadMarkerView
import im.vector.app.core.ui.views.NotificationAreaView
import im.vector.app.core.utils.Debouncer
@ -110,6 +114,7 @@ import im.vector.app.features.attachments.toGroupedContentAttachmentData
import im.vector.app.features.call.SharedActiveCallViewModel
import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.WebRtcPeerConnectionManager
import im.vector.app.features.call.conference.JitsiCallViewModel
import im.vector.app.features.command.Command
import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
import im.vector.app.features.crypto.util.toImageRes
@ -129,7 +134,6 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationD
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
import im.vector.app.features.home.room.detail.widget.RoomWidgetsBannerView
import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet
import im.vector.app.features.home.room.detail.widget.WidgetRequestCodes
import im.vector.app.features.html.EventHtmlRenderer
@ -147,6 +151,17 @@ import im.vector.app.features.settings.VectorSettingsActivity
import im.vector.app.features.share.SharedData
import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.widgets.WidgetActivity
import im.vector.app.features.widgets.WidgetArgs
import im.vector.app.features.widgets.WidgetKind
import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_detail.*
import kotlinx.android.synthetic.main.merge_composer_layout.view.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import org.billcarsonfr.jsonviewer.JSonViewerDialog
import org.commonmark.parser.Parser
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.permalinks.PermalinkFactory
import org.matrix.android.sdk.api.session.Session
@ -169,20 +184,13 @@ import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
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.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_detail.*
import kotlinx.android.synthetic.main.merge_composer_layout.view.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import org.billcarsonfr.jsonviewer.JSonViewerDialog
import org.commonmark.parser.Parser
import timber.log.Timber
import java.io.File
import java.net.URL
@ -217,7 +225,7 @@ class RoomDetailFragment @Inject constructor(
JumpToReadMarkerView.Callback,
AttachmentTypeSelectorView.Callback,
AttachmentsHelper.Callback,
RoomWidgetsBannerView.Callback,
// RoomWidgetsBannerView.Callback,
ActiveCallView.Callback {
companion object {
@ -292,7 +300,7 @@ class RoomDetailFragment @Inject constructor(
setupJumpToReadMarkerView()
setupActiveCallView()
setupJumpToBottomView()
setupWidgetsBannerView()
setupConfBannerView()
roomToolbarContentView.debouncedClicks {
navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
@ -350,10 +358,44 @@ class RoomDetailFragment @Inject constructor(
is RoomDetailViewEvents.DisplayEnableIntegrationsWarning -> displayDisabledIntegrationDialog()
is RoomDetailViewEvents.OpenIntegrationManager -> openIntegrationManager()
is RoomDetailViewEvents.OpenFile -> startOpenFileIntent(it)
RoomDetailViewEvents.OpenActiveWidgetBottomSheet -> onViewWidgetsClicked()
is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message)
is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo)
RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView()
RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView()
is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it)
}.exhaustive
}
}
private fun requestNativeWidgetPermission(it: RoomDetailViewEvents.RequestNativeWidgetPermission) {
val tag = RoomWidgetPermissionBottomSheet::class.java.name
val dFrag = childFragmentManager.findFragmentByTag(tag) as? RoomWidgetPermissionBottomSheet
if (dFrag != null && dFrag.dialog?.isShowing == true && !dFrag.isRemoving) {
return
} else {
RoomWidgetPermissionBottomSheet.newInstance(
WidgetArgs(
baseUrl = it.domain,
kind = WidgetKind.ROOM,
roomId = roomDetailArgs.roomId,
widgetId = it.widget.widgetId
)
).apply {
directListener = { granted ->
if (granted) {
roomDetailViewModel.handle(RoomDetailAction.EnsureNativeWidgetAllowed(
widget = it.widget,
userJustAccepted = true,
grantedEvents = it.grantedEvents
))
}
}
}
.show(childFragmentManager, tag)
}
}
private fun openIntegrationManager(screen: String? = null) {
navigator.openIntegrationManager(
fragment = this,
@ -363,8 +405,33 @@ class RoomDetailFragment @Inject constructor(
)
}
private fun setupWidgetsBannerView() {
roomWidgetsBannerView.callback = this
private fun setupConfBannerView() {
activeConferenceView.callback = object : ActiveConferenceView.Callback {
override fun onTapJoinAudio(jitsiWidget: Widget) {
// need to check if allowed first
roomDetailViewModel.handle(RoomDetailAction.EnsureNativeWidgetAllowed(
widget = jitsiWidget,
userJustAccepted = false,
grantedEvents = RoomDetailViewEvents.JoinJitsiConference(jitsiWidget, false))
)
}
override fun onTapJoinVideo(jitsiWidget: Widget) {
roomDetailViewModel.handle(RoomDetailAction.EnsureNativeWidgetAllowed(
widget = jitsiWidget,
userJustAccepted = false,
grantedEvents = RoomDetailViewEvents.JoinJitsiConference(jitsiWidget, true))
)
}
override fun onDelete(jitsiWidget: Widget) {
roomDetailViewModel.handle(RoomDetailAction.RemoveWidget(jitsiWidget.widgetId))
}
}
}
private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) {
navigator.openRoomWidget(requireContext(), roomDetailArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to enableVideo))
}
private fun openStickerPicker(event: RoomDetailViewEvents.OpenStickerPicker) {
@ -529,10 +596,40 @@ class RoomDetailFragment @Inject constructor(
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
// We use a custom layout for this menu item, so we need to set a ClickListener
menu.findItem(R.id.open_matrix_apps)?.let { menuItem ->
menuItem.actionView.setOnClickListener {
onOptionsItemSelected(menuItem)
}
}
}
override fun onPrepareOptionsMenu(menu: Menu) {
menu.forEach {
it.isVisible = roomDetailViewModel.isMenuItemVisible(it.itemId)
}
withState(roomDetailViewModel) { state ->
val matrixAppsMenuItem = menu.findItem(R.id.open_matrix_apps)
val widgetsCount = state.activeRoomWidgets.invoke()?.size ?: 0
if (widgetsCount > 0) {
val actionView = matrixAppsMenuItem.actionView
actionView
.findViewById<ImageView>(R.id.action_view_icon_image)
.setColorFilter(ContextCompat.getColor(requireContext(), R.color.riotx_accent))
actionView.findViewById<TextView>(R.id.cart_badge).setTextOrHide("$widgetsCount")
matrixAppsMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
} else {
// icon should be default color no badge
val actionView = matrixAppsMenuItem.actionView
actionView
.findViewById<ImageView>(R.id.action_view_icon_image)
.setColorFilter(ThemeUtils.getColor(requireContext(), R.attr.riotx_text_secondary))
actionView.findViewById<TextView>(R.id.cart_badge).isVisible = false
matrixAppsMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -549,26 +646,12 @@ class RoomDetailFragment @Inject constructor(
true
}
R.id.open_matrix_apps -> {
roomDetailViewModel.handle(RoomDetailAction.OpenIntegrationManager)
roomDetailViewModel.handle(RoomDetailAction.ManageIntegrations)
true
}
R.id.voice_call,
R.id.video_call -> {
val activeCall = sharedCallActionViewModel.activeCall.value
val isVideoCall = item.itemId == R.id.video_call
if (activeCall != null) {
// resume existing if same room, if not prompt to kill and then restart new call?
if (activeCall.roomId == roomDetailArgs.roomId) {
onTapToReturnToCall()
}
// else {
// TODO might not work well, and should prompt
// webRtcPeerConnectionManager.endCall()
// safeStartCall(it, isVideoCall)
// }
} else {
safeStartCall(isVideoCall)
}
handleCallRequest(item)
true
}
R.id.hangup_call -> {
@ -579,6 +662,62 @@ class RoomDetailFragment @Inject constructor(
}
}
private fun handleCallRequest(item: MenuItem) = withState(roomDetailViewModel) { state ->
val roomSummary = state.asyncRoomSummary.invoke() ?: return@withState
val isVideoCall = item.itemId == R.id.video_call
when (roomSummary.joinedMembersCount) {
1 -> {
val pendingInvite = roomSummary.invitedMembersCount ?: 0 > 0
if (pendingInvite) {
// wait for other to join
showDialogWithMessage(getString(R.string.cannot_call_yourself_with_invite))
} else {
// You cannot place a call with yourself.
showDialogWithMessage(getString(R.string.cannot_call_yourself))
}
}
2 -> {
val activeCall = sharedCallActionViewModel.activeCall.value
if (activeCall != null) {
// resume existing if same room, if not prompt to kill and then restart new call?
if (activeCall.roomId == roomDetailArgs.roomId) {
onTapToReturnToCall()
}
// else {
// TODO might not work well, and should prompt
// webRtcPeerConnectionManager.endCall()
// safeStartCall(it, isVideoCall)
// }
} else {
safeStartCall(isVideoCall)
}
}
else -> {
// it's jitsi call
// can you add widgets??
if (!state.isAllowedToManageWidgets) {
// You do not have permission to start a conference call in this room
showDialogWithMessage(getString(R.string.no_permissions_to_start_conf_call))
} else {
if (state.activeRoomWidgets()?.filter { it.type == WidgetType.Jitsi }?.any() == true) {
// A conference is already in progress!
showDialogWithMessage(getString(R.string.conference_call_in_progress))
} else {
AlertDialog.Builder(requireContext())
.setTitle(if (isVideoCall) R.string.video_meeting else R.string.audio_meeting)
.setMessage(R.string.audio_video_meeting_description)
.setPositiveButton(getString(R.string.create)) { _, _ ->
// create the widget, then navigate to it..
roomDetailViewModel.handle(RoomDetailAction.AddJitsiWidget(isVideoCall))
}
.setNegativeButton(getString(R.string.cancel), null)
.show()
}
}
}
}
}
private fun displayDisabledIntegrationDialog() {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.disabled_integration_dialog_title)
@ -871,9 +1010,9 @@ class RoomDetailFragment @Inject constructor(
invalidateOptionsMenu()
val summary = state.asyncRoomSummary()
renderToolbar(summary, state.typingMessage)
activeConferenceView.render(state)
val inviter = state.asyncInviter()
if (summary?.membership == Membership.JOIN) {
roomWidgetsBannerView.render(state.activeRoomWidgets())
jumpToBottomView.count = summary.notificationCount
jumpToBottomView.drawBadge = summary.hasUnreadMessages
scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline
@ -1111,7 +1250,7 @@ class RoomDetailFragment @Inject constructor(
}
}
// TimelineEventController.Callback ************************************************************
// TimelineEventController.Callback ************************************************************
override fun onUrlClicked(url: String, title: String): Boolean {
permalinkHandler
@ -1583,7 +1722,14 @@ class RoomDetailFragment @Inject constructor(
Snackbar.make(requireView(), message, duration).show()
}
// VectorInviteView.Callback
private fun showDialogWithMessage(message: String) {
AlertDialog.Builder(requireContext())
.setMessage(message)
.setPositiveButton(getString(R.string.ok), null)
.show()
}
// VectorInviteView.Callback
override fun onAcceptInvite() {
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
@ -1595,7 +1741,7 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(RoomDetailAction.RejectInvite)
}
// JumpToReadMarkerView.Callback
// JumpToReadMarkerView.Callback
override fun onJumpToReadMarkerClicked() = withState(roomDetailViewModel) {
jumpToReadMarkerView.isVisible = false
@ -1611,7 +1757,7 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(RoomDetailAction.MarkAllAsRead)
}
// AttachmentTypeSelectorView.Callback
// AttachmentTypeSelectorView.Callback
override fun onTypeSelected(type: AttachmentTypeSelectorView.Type) {
if (checkPermissions(type.permissionsBit, this, PERMISSION_REQUEST_CODE_PICK_ATTACHMENT)) {
@ -1632,7 +1778,7 @@ class RoomDetailFragment @Inject constructor(
}.exhaustive
}
// AttachmentsHelper.Callback
// AttachmentsHelper.Callback
override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) {
if (roomDetailViewModel.preventAttachmentPreview) {
@ -1662,7 +1808,7 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(RoomDetailAction.SendMessage(formattedContact, false))
}
override fun onViewWidgetsClicked() {
private fun onViewWidgetsClicked() {
RoomWidgetsBottomSheet.newInstance()
.show(childFragmentManager, "ROOM_WIDGETS_BOTTOM_SHEET")
}

View file

@ -35,9 +35,14 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
data class ActionFailure(val action: RoomDetailAction, val throwable: Throwable) : RoomDetailViewEvents()
data class ShowMessage(val message: String) : RoomDetailViewEvents()
data class ShowInfoOkDialog(val message: String) : RoomDetailViewEvents()
data class ShowE2EErrorMessage(val withHeldCode: WithHeldCode?) : RoomDetailViewEvents()
data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents()
data class JoinJitsiConference(val widget: Widget, val withVideo: Boolean) : RoomDetailViewEvents()
object ShowWaitingView: RoomDetailViewEvents()
object HideWaitingView: RoomDetailViewEvents()
data class FileTooBigError(
val filename: String,
@ -66,6 +71,10 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
data class OpenStickerPicker(val widget: Widget): RoomDetailViewEvents()
object OpenIntegrationManager: RoomDetailViewEvents()
object OpenActiveWidgetBottomSheet: RoomDetailViewEvents()
data class RequestNativeWidgetPermission(val widget: Widget,
val domain: String,
val grantedEvents: RoomDetailViewEvents) : RoomDetailViewEvents()
object MessageSent : SendMessageResult()
data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult()

View file

@ -43,7 +43,17 @@ import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
import im.vector.app.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import im.vector.app.features.settings.VectorLocale
import im.vector.app.features.settings.VectorPreferences
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import io.reactivex.rxkotlin.subscribeBy
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.NoOpMatrixCallback
@ -76,23 +86,18 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent
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.toOptional
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import io.reactivex.rxkotlin.subscribeBy
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import timber.log.Timber
import java.io.File
import java.util.UUID
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
@ -188,8 +193,12 @@ class RoomDetailViewModel @AssistedInject constructor(
PowerLevelsObservableFactory(room).createObservable()
.subscribe {
val canSendMessage = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE)
val isAllowedToManageWidgets = session.widgetService().hasPermissionsToHandleWidgets(room.roomId)
setState {
copy(canSendMessage = canSendMessage)
copy(
canSendMessage = canSendMessage,
isAllowedToManageWidgets = isAllowedToManageWidgets
)
}
}
.disposeOnClear()
@ -269,6 +278,10 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
is RoomDetailAction.StartCall -> handleStartCall(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)
}.exhaustive
}
@ -306,6 +319,115 @@ class RoomDetailViewModel @AssistedInject constructor(
}
}
private fun handleManageIntegrations() = withState { state ->
if (state.activeRoomWidgets().isNullOrEmpty()) {
// Directly open integration manager screen
handleOpenIntegrationManager()
} else {
// Display bottomsheet with widget list
_viewEvents.post(RoomDetailViewEvents.OpenActiveWidgetBottomSheet)
}
}
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.toLowerCase(VectorLocale.applicationLocale)
val jitsiDomain = session.getHomeServerCapabilities().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
val url = "https://app.element.io/jitsi.html" +
"?confId=$confId" +
"#conferenceDomain=\$domain" +
"&conferenceId=\$conferenceId" +
"&isAudioOnly=${!action.withVideo}" +
"&displayName=\$matrix_display_name" +
"&avatarUrl=\$matrix_avatar_url" +
"&userId=\$matrix_user_id"
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 = awaitCallback<Widget> {
session.widgetService().createRoomWidget(roomId, widgetId, widgetEventContent, it)
}
_viewEvents.post(RoomDetailViewEvents.JoinJitsiConference(widget, action.withVideo))
} catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.failed_to_add_widget)))
} finally {
_viewEvents.post(RoomDetailViewEvents.HideWaitingView)
}
}
}
private fun handleDeleteWidget(widgetId: String) {
_viewEvents.post(RoomDetailViewEvents.ShowWaitingView)
viewModelScope.launch(Dispatchers.IO) {
try {
awaitCallback<Unit> { session.widgetService().destroyRoomWidget(room.roomId, widgetId, it) }
// local echo
setState {
copy(
activeRoomWidgets = when (activeRoomWidgets) {
is Success -> {
Success(activeRoomWidgets.invoke().filter { it.widgetId != widgetId })
}
else -> activeRoomWidgets
}
)
}
} catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.failed_to_remove_widget)))
} finally {
_viewEvents.post(RoomDetailViewEvents.HideWaitingView)
}
}
}
private fun handleCheckWidgetAllowed(action: RoomDetailAction.EnsureNativeWidgetAllowed) {
val widget = action.widget
val domain = action.widget.widgetContent.data["domain"] as? String ?: ""
val isAllowed = action.userJustAccepted || if (widget.type == WidgetType.Jitsi) {
widget.senderInfo?.userId == session.myUserId
|| session.integrationManagerService().isNativeWidgetDomainAllowed(
action.widget.type.preferred,
domain
)
} else false
if (isAllowed) {
_viewEvents.post(action.grantedEvents)
} else {
// we need to request permission
_viewEvents.post(RoomDetailViewEvents.RequestNativeWidgetPermission(widget, domain, action.grantedEvents))
}
}
private fun startTrackingUnreadMessages() {
trackUnreadMessages.set(true)
setState { copy(canShowJumpToReadMarker = false) }
@ -419,7 +541,7 @@ class RoomDetailViewModel @AssistedInject constructor(
R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true
R.id.open_matrix_apps -> true
R.id.voice_call,
R.id.video_call -> state.asyncRoomSummary()?.canStartCall == true && webRtcPeerConnectionManager.currentCall == null
R.id.video_call -> true // always show for discoverability
R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null
else -> false
}

View file

@ -66,7 +66,8 @@ data class RoomDetailViewState(
val unreadState: UnreadState = UnreadState.Unknown,
val canShowJumpToReadMarker: Boolean = true,
val changeMembershipState: ChangeMembershipState = ChangeMembershipState.Unknown,
val canSendMessage: Boolean = true
val canSendMessage: Boolean = true,
val isAllowedToManageWidgets: Boolean = false
) : MvRxState {
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)

View file

@ -1,43 +0,0 @@
/*
* 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.home.room.detail.widget
import com.airbnb.epoxy.TypedEpoxyController
import org.matrix.android.sdk.api.session.widgets.model.Widget
import javax.inject.Inject
/**
* Epoxy controller for room widgets list
*/
class RoomWidgetController @Inject constructor() : TypedEpoxyController<List<Widget>>() {
var listener: Listener? = null
override fun buildModels(widget: List<Widget>) {
widget.forEach {
RoomWidgetItem_()
.id(it.widgetId)
.widget(it)
.widgetClicked { listener?.didSelectWidget(it) }
.addTo(this)
}
}
interface Listener {
fun didSelectWidget(widget: Widget)
}
}

View file

@ -16,7 +16,10 @@
package im.vector.app.features.home.room.detail.widget
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
@ -24,21 +27,34 @@ import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.onClick
import org.matrix.android.sdk.api.extensions.tryThis
import org.matrix.android.sdk.api.session.widgets.model.Widget
import java.net.URL
@EpoxyModelClass(layout = R.layout.item_room_widget)
abstract class RoomWidgetItem : EpoxyModelWithHolder<RoomWidgetItem.Holder>() {
@EpoxyAttribute lateinit var widget: Widget
@EpoxyAttribute var widgetClicked: ClickListener? = null
@DrawableRes
@EpoxyAttribute var iconRes: Int? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.widgetName.text = widget.name
holder.widgetUrl.text = tryThis { URL(widget.computedUrl) }?.host ?: widget.computedUrl
if (iconRes != null) {
holder.iconImage.isVisible = true
holder.iconImage.setImageResource(iconRes!!)
} else {
holder.iconImage.isVisible = false
}
holder.view.onClick(widgetClicked)
}
class Holder : VectorEpoxyHolder() {
val widgetName by bind<TextView>(R.id.roomWidgetName)
val widgetUrl by bind<TextView>(R.id.roomWidgetUrl)
val iconImage by bind<ImageView>(R.id.roomWidgetAvatar)
}
}

View file

@ -27,6 +27,7 @@ import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.core.resources.ColorProvider
import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.RoomDetailViewModel
import im.vector.app.features.home.room.detail.RoomDetailViewState
import im.vector.app.features.navigation.Navigator
@ -37,9 +38,9 @@ import javax.inject.Inject
/**
* Bottom sheet displaying active widgets in a room
*/
class RoomWidgetsBottomSheet : VectorBaseBottomSheetDialogFragment(), RoomWidgetController.Listener {
class RoomWidgetsBottomSheet : VectorBaseBottomSheetDialogFragment(), RoomWidgetsController.Listener {
@Inject lateinit var epoxyController: RoomWidgetController
@Inject lateinit var epoxyController: RoomWidgetsController
@Inject lateinit var colorProvider: ColorProvider
@Inject lateinit var navigator: Navigator
@ -77,6 +78,11 @@ class RoomWidgetsBottomSheet : VectorBaseBottomSheetDialogFragment(), RoomWidget
dismiss()
}
override fun didSelectManageWidgets() {
roomDetailViewModel.handle(RoomDetailAction.OpenIntegrationManager)
dismiss()
}
companion object {
fun newInstance(): RoomWidgetsBottomSheet {
return RoomWidgetsBottomSheet()

View file

@ -0,0 +1,66 @@
/*
* 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.home.room.detail.widget
import android.view.View
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.R
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericButtonItem
import im.vector.app.core.ui.list.genericFooterItem
import org.matrix.android.sdk.api.session.widgets.model.Widget
import javax.inject.Inject
/**
* Epoxy controller for room widgets list
*/
class RoomWidgetsController @Inject constructor(
val stringProvider: StringProvider,
val colorProvider: ColorProvider)
: TypedEpoxyController<List<Widget>>() {
var listener: Listener? = null
override fun buildModels(widgets: List<Widget>) {
if (widgets.isEmpty()) {
genericFooterItem {
id("empty")
text(stringProvider.getString(R.string.room_no_active_widgets))
}
} else {
widgets.forEach {
roomWidgetItem {
id(it.widgetId)
widget(it)
widgetClicked { listener?.didSelectWidget(it) }
}
}
}
genericButtonItem {
id("addIntegration")
text(stringProvider.getString(R.string.room_manage_integrations))
textColor(colorProvider.getColor(R.color.riotx_accent))
buttonClickAction(View.OnClickListener { listener?.didSelectManageWidgets() })
}
}
interface Listener {
fun didSelectWidget(widget: Widget)
fun didSelectManageWidgets()
}
}

View file

@ -31,6 +31,8 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.error.fatalError
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.toast
import im.vector.app.features.call.conference.JitsiCallViewModel
import im.vector.app.features.call.conference.VectorJitsiActivity
import im.vector.app.features.createdirect.CreateDirectRoomActivity
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity
@ -66,6 +68,7 @@ import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom
import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData
import org.matrix.android.sdk.api.session.terms.TermsService
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.MatrixItem
import javax.inject.Inject
import javax.inject.Singleton
@ -270,9 +273,14 @@ class DefaultNavigator @Inject constructor(
fragment.startActivityForResult(intent, WidgetRequestCodes.INTEGRATION_MANAGER_REQUEST_CODE)
}
override fun openRoomWidget(context: Context, roomId: String, widget: Widget) {
val widgetArgs = widgetArgsBuilder.buildRoomWidgetArgs(roomId, widget)
context.startActivity(WidgetActivity.newIntent(context, widgetArgs))
override fun openRoomWidget(context: Context, roomId: String, widget: Widget, options: Map<String, Any>?) {
if (widget.type is WidgetType.Jitsi) {
val enableVideo = options?.get(JitsiCallViewModel.ENABLE_VIDEO_OPTION) == true
context.startActivity(VectorJitsiActivity.newIntent(context, roomId = roomId, widgetId = widget.widgetId, enableVideo = enableVideo))
} else {
val widgetArgs = widgetArgsBuilder.buildRoomWidgetArgs(roomId, widget)
context.startActivity(WidgetActivity.newIntent(context, widgetArgs))
}
}
override fun openPinCode(fragment: Fragment, pinMode: PinMode, requestCode: Int) {

View file

@ -97,7 +97,7 @@ interface Navigator {
fun openIntegrationManager(fragment: Fragment, roomId: String, integId: String?, screen: String?)
fun openRoomWidget(context: Context, roomId: String, widget: Widget)
fun openRoomWidget(context: Context, roomId: String, widget: Widget, options: Map<String, Any>? = null)
fun openMediaViewer(activity: Activity,
roomId: String,

View file

@ -111,7 +111,7 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
// id("complete_security")
// iconRes(R.drawable.ic_shield_warning)
// text(stringProvider.getString(R.string.complete_security))
// itemClickAction(DebouncedClickListener(View.OnClickListener { _ ->
// buttonClickAction(DebouncedClickListener(View.OnClickListener { _ ->
// callback?.completeSecurity()
// }))
// }

View file

@ -48,6 +48,9 @@ class RoomWidgetPermissionBottomSheet : VectorBaseBottomSheetDialogFragment() {
injector.inject(this)
}
// Use this if you don't need the full activity view model
var directListener: ((Boolean) -> Unit)? = null
override fun invalidate() = withState(viewModel) { state ->
super.invalidate()
val permissionData = state.permissionData() ?: return@withState
@ -88,6 +91,7 @@ class RoomWidgetPermissionBottomSheet : VectorBaseBottomSheetDialogFragment() {
@OnClick(R.id.widgetPermissionDecline)
fun doDecline() {
viewModel.handle(RoomWidgetPermissionActions.BlockWidget)
directListener?.invoke(false)
// optimistic dismiss
dismiss()
}
@ -95,6 +99,7 @@ class RoomWidgetPermissionBottomSheet : VectorBaseBottomSheetDialogFragment() {
@OnClick(R.id.widgetPermissionContinue)
fun doAccept() {
viewModel.handle(RoomWidgetPermissionActions.AllowWidget)
directListener?.invoke(true)
// optimistic dismiss
dismiss()
}

View file

@ -28,6 +28,8 @@ import org.matrix.android.sdk.api.session.Session
import im.vector.app.R
import im.vector.app.core.platform.VectorViewModel
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.rx.rx
import timber.log.Timber
import java.net.URL
@ -57,20 +59,33 @@ class RoomWidgetPermissionViewModel @AssistedInject constructor(@Assisted val in
}
// TODO check from widget urls the perms that should be shown?
// For now put all
val infoShared = listOf(
R.string.room_widget_permission_display_name,
R.string.room_widget_permission_avatar_url,
R.string.room_widget_permission_user_id,
R.string.room_widget_permission_theme,
R.string.room_widget_permission_widget_id,
R.string.room_widget_permission_room_id
)
RoomWidgetPermissionViewState.WidgetPermissionData(
widget = widget,
isWebviewWidget = true,
permissionsList = infoShared,
widgetDomain = domain
)
if (widget.type == WidgetType.Jitsi) {
val infoShared = listOf(
R.string.room_widget_permission_display_name,
R.string.room_widget_permission_avatar_url
)
RoomWidgetPermissionViewState.WidgetPermissionData(
widget = widget,
isWebviewWidget = false,
permissionsList = infoShared,
widgetDomain = widget.widgetContent.data["domain"] as? String
)
} else {
val infoShared = listOf(
R.string.room_widget_permission_display_name,
R.string.room_widget_permission_avatar_url,
R.string.room_widget_permission_user_id,
R.string.room_widget_permission_theme,
R.string.room_widget_permission_widget_id,
R.string.room_widget_permission_room_id
)
RoomWidgetPermissionViewState.WidgetPermissionData(
widget = widget,
isWebviewWidget = true,
permissionsList = infoShared,
widgetDomain = domain
)
}
}
.execute {
copy(permissionData = it)
@ -91,7 +106,14 @@ class RoomWidgetPermissionViewModel @AssistedInject constructor(@Assisted val in
if (state.permissionData()?.isWebviewWidget.orFalse()) {
WidgetPermissionsHelper(integrationManagerService, widgetService).changePermission(state.roomId, widgetId, false)
} else {
// TODO JITSI
awaitCallback<Unit> {
session.integrationManagerService().setNativeWidgetDomainAllowed(
state.permissionData.invoke()?.widget?.type?.preferred ?: "",
state.permissionData.invoke()?.widgetDomain ?: "",
false,
it
)
}
}
} catch (failure: Throwable) {
Timber.v("Failure revoking widget: ${state.widgetId}")
@ -109,7 +131,14 @@ class RoomWidgetPermissionViewModel @AssistedInject constructor(@Assisted val in
if (state.permissionData()?.isWebviewWidget.orFalse()) {
WidgetPermissionsHelper(integrationManagerService, widgetService).changePermission(state.roomId, widgetId, true)
} else {
// TODO JITSI
awaitCallback<Unit> {
session.integrationManagerService().setNativeWidgetDomainAllowed(
state.permissionData.invoke()?.widget?.type?.preferred ?: "",
state.permissionData.invoke()?.widgetDomain ?: "",
true,
it
)
}
}
} catch (failure: Throwable) {
Timber.v("Failure allowing widget: ${state.widgetId}")

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M1,5C1,2.7909 2.7909,1 5,1H19C21.2091,1 23,2.7909 23,5V19C23,21.2091 21.2091,23 19,23H5C2.7909,23 1,21.2091 1,19V5ZM11,7.5C11,9.433 9.433,11 7.5,11C5.567,11 4,9.433 4,7.5C4,5.567 5.567,4 7.5,4C9.433,4 11,5.567 11,7.5ZM7.5,20C9.433,20 11,18.433 11,16.5C11,14.567 9.433,13 7.5,13C5.567,13 4,14.567 4,16.5C4,18.433 5.567,20 7.5,20ZM20,16.5C20,18.433 18.433,20 16.5,20C14.567,20 13,18.433 13,16.5C13,14.567 14.567,13 16.5,13C18.433,13 20,14.567 20,16.5ZM16.5,11C18.433,11 20,9.433 20,7.5C20,5.567 18.433,4 16.5,4C14.567,4 13,5.567 13,7.5C13,9.433 14.567,11 16.5,11Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/jitsi_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black">
<!-- Note: A org.jitsi.meet.sdk.JitsiMeetView will be added here -->
<LinearLayout
android:id="@+id/jitsi_progress_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical">
<ProgressBar
android:layout_width="40dp"
android:layout_height="40dp"
android:indeterminate="true" />
</LinearLayout>
</FrameLayout>

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
style="?attr/actionButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:focusable="true">
<ImageView
android:id="@+id/action_view_icon_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_integrations"
android:tint="@color/riotx_accent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/cart_badge"
android:layout_width="12dp"
android:layout_height="12dp"
android:background="@drawable/bg_unread_highlight"
android:gravity="center"
android:textColor="@android:color/white"
android:textSize="8sp"
app:layout_constraintCircle="@+id/action_view_icon_image"
app:layout_constraintCircleAngle="45"
app:layout_constraintCircleRadius="12dp"
tools:ignore="MissingConstraints"
tools:text="8" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -104,6 +104,14 @@
app:layout_constraintTop_toBottomOf="@id/syncStateView"
tools:visibility="visible" />
<im.vector.app.core.ui.views.ActiveConferenceView
android:id="@+id/activeConferenceView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/activeCallView"
tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
@ -112,7 +120,7 @@
app:layout_constraintBottom_toTopOf="@+id/recyclerViewBarrier"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/activeCallView"
app:layout_constraintTop_toBottomOf="@id/activeConferenceView"
tools:listitem="@layout/item_timeline_event_base" />
<FrameLayout
@ -121,17 +129,17 @@
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/activeCallView">
app:layout_constraintTop_toBottomOf="@id/activeConferenceView">
<im.vector.app.features.home.room.detail.widget.RoomWidgetsBannerView
android:id="@+id/roomWidgetsBannerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:visibility="gone"
tools:visibility="visible" />
<!-- <im.vector.app.features.home.room.detail.widget.RoomWidgetsBannerView-->
<!-- android:id="@+id/roomWidgetsBannerView"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginStart="8dp"-->
<!-- android:layout_marginTop="8dp"-->
<!-- android:layout_marginEnd="8dp"-->
<!-- android:visibility="gone"-->
<!-- tools:visibility="visible" />-->
<im.vector.app.core.ui.views.JumpToReadMarkerView
android:id="@+id/jumpToReadMarkerView"
@ -190,7 +198,7 @@
android:focusable="true"
app:cardCornerRadius="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/activeCallView">
app:layout_constraintTop_toBottomOf="@id/activeConferenceView">
<org.webrtc.SurfaceViewRenderer
android:id="@+id/activeCallPiP"

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -16,11 +16,32 @@
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/roomWidgetName"
style="@style/BottomSheetItemTextMain"
tools:text="@sample/matrix.json/data/displayName" />
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toTopOf="@id/roomWidgetUrl"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/roomWidgetAvatar"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Widget name" />
</LinearLayout>
<TextView
android:id="@+id/roomWidgetUrl"
style="@style/BottomSheetItemTextSecondary"
android:textStyle="normal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/roomWidgetName"
app:layout_constraintStart_toStartOf="@id/roomWidgetName"
app:layout_constraintTop_toBottomOf="@id/roomWidgetName"
tools:text="https://foobar" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -25,8 +25,9 @@
android:textColor="@color/white"
app:drawableTint="@color/white" />
<TextView
<com.google.android.material.button.MaterialButton
android:id="@+id/returnToCallButton"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/activeCallInfo"
@ -38,7 +39,6 @@
android:paddingStart="8dp"
android:paddingEnd="16dp"
android:text="@string/return_to_call"
android:textAllCaps="true"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold" />

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorPrimary"
tools:parentTag="android.widget.RelativeLayout">
<TextView
android:id="@+id/activeConferenceInfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toStartOf="@id/deleteWidgetButton"
android:drawableStart="@drawable/ic_call"
android:drawablePadding="10dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingTop="12dp"
android:paddingEnd="16dp"
android:paddingBottom="12dp"
android:textColor="@color/white"
android:textColorLink="@color/white"
app:drawableTint="@color/white"
tools:text="@string/ongoing_conference_call" />
<com.google.android.material.button.MaterialButton
android:id="@+id/deleteWidgetButton"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/activeConferenceInfo"
android:layout_alignBottom="@+id/activeConferenceInfo"
android:layout_alignParentEnd="true"
android:clickable="false"
android:focusable="false"
android:gravity="center"
android:paddingStart="8dp"
android:paddingEnd="16dp"
android:text="@string/action_close"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold" />
</merge>

View file

@ -12,11 +12,6 @@
app:showAsAction="always"
tools:visible="true" />
<item
android:id="@+id/open_matrix_apps"
android:title="@string/room_add_matrix_apps"
app:showAsAction="never" />
<item
android:id="@+id/voice_call"
android:icon="@drawable/ic_phone"
@ -35,6 +30,12 @@
app:showAsAction="always"
tools:visible="true" />
<item
android:id="@+id/open_matrix_apps"
android:title="@string/room_add_matrix_apps"
app:actionLayout="@layout/custom_action_item_layout_badge"
app:showAsAction="ifRoom" />
<item
android:id="@+id/resend_all"
android:icon="@drawable/ic_refresh_cw"

View file

@ -18,6 +18,9 @@
<!-- Note: pusher_app_id cannot exceed 64 chars -->
<string name="pusher_app_id" translatable="false">im.vector.app.android</string>
<!-- preferred jitsi domain -->
<string name="preferred_jitsi_domain" translatable="false">jitsi.riot.im</string>
<string-array name="room_directory_servers" translatable="false">
<item>matrix.org</item>
</string-array>

View file

@ -89,8 +89,17 @@
<string name="missing_permissions_warning">"Due to missing permissions, some features may be missing…</string>
<string name="missing_permissions_error">"Due to missing permissions, this action is not possible.</string>
<string name="missing_permissions_to_start_conf_call">You need permission to invite to start a conference in this room</string>
<string name="no_permissions_to_start_conf_call">You do not have permission to start a conference call in this room</string>
<string name="conference_call_in_progress">A conference is already in progress!</string>
<string name="video_meeting">Start video meeting</string>
<string name="audio_meeting">Start audio meeting</string>
<string name="audio_video_meeting_description">Meetings use Jitsi security and permission policies. All people currently in the room will see an invite to join while your meeting is happening.</string>
<string name="missing_permissions_title_to_start_conf_call">Cannot start call</string>
<string name="cannot_call_yourself">You cannot place a call with yourself</string>
<string name="cannot_call_yourself_with_invite">You cannot place a call with yourself, wait for participants to accept invitation</string>
<string name="device_information">Session information</string>
<string name="failed_to_add_widget">Failed to add widget</string>
<string name="failed_to_remove_widget">Failed to remove widget</string>
<string name="room_no_conference_call_in_encrypted_rooms">Conference calls are not supported in encrypted rooms</string>
<string name="call_anyway">Call Anyway</string>
<string name="send_anyway">Send Anyway</string>
@ -1210,6 +1219,8 @@
<string name="widget_integration_invalid_parameter">A parameter is not valid.</string>
<string name="integration_manager_not_configured">No integration manager configured.</string>
<string name="room_add_matrix_apps">Add Matrix apps</string>
<string name="room_manage_integrations">Manage Integrations</string>
<string name="room_no_active_widgets">No active widgets</string>
<string name="settings_labs_native_camera">Use native camera</string>
<string name="settings_labs_native_camera_summary">Start the system camera instead of the custom camera screen.</string>
<string name="settings_labs_keyboard_options_to_send_message">Use keyboard enter key to send message</string>

View file

@ -331,6 +331,17 @@
<item name="android:textSize">16sp</item>
</style>
<style name="BottomSheetItemTextSecondary">
<item name="android:fontFamily">sans-serif</item>
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_weight">1</item>
<item name="android:ellipsize">end</item>
<item name="android:maxLines">2</item>
<item name="android:textColor">?riotx_text_secondary</item>
<item name="android:textSize">14sp</item>
</style>
<style name="BottomSheetItemTime">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>