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>gplay</w>
<w>hmac</w> <w>hmac</w>
<w>homeserver</w> <w>homeserver</w>
<w>jitsi</w>
<w>ktlint</w> <w>ktlint</w>
<w>linkified</w> <w>linkified</w>
<w>linkify</w> <w>linkify</w>

View File

@ -3,6 +3,7 @@ Changes in Element 1.0.5 (2020-XX-XX)
Features ✨: Features ✨:
- Protect access to the app by a pin code (#1700) - Protect access to the app by a pin code (#1700)
- Conference with Jitsi support (#43)
Improvements 🙌: Improvements 🙌:
- Give user the possibility to prevent accidental call (#1869) - 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/' } 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() google()
jcenter() 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:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version" implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
implementation "com.squareup.retrofit2:converter-scalars:$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" implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
@ -174,8 +179,10 @@ dependencies {
implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23' implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23'
// Web RTC // Web RTC
// TODO meant for development purposes only. See http://webrtc.github.io/webrtc-org/native-code/android/ // 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.+' // 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' debugImplementation 'com.airbnb.okreplay:okreplay:1.5.0'
releaseImplementation 'com.airbnb.okreplay:noop: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> * </pre>
*/ */
@ -57,7 +60,10 @@ data class WellKnown(
val integrations: JsonDict? = null, val integrations: JsonDict? = null,
@Json(name = "im.vector.riot.e2ee") @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") @Json(name = "default")
val e2eDefault: Boolean = true 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 * 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. * (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 { companion object {
const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L

View File

@ -17,10 +17,10 @@
package org.matrix.android.sdk.internal.database 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.DynamicRealm
import io.realm.RealmMigration 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 timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -31,6 +31,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
if (oldVersion <= 0) migrateTo1(realm) if (oldVersion <= 0) migrateTo1(realm)
if (oldVersion <= 1) migrateTo2(realm) if (oldVersion <= 1) migrateTo2(realm)
if (oldVersion <= 2) migrateTo3(realm)
} }
private fun migrateTo1(realm: DynamicRealm) { private fun migrateTo1(realm: DynamicRealm) {
@ -52,4 +53,14 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
obj.setBoolean(HomeServerCapabilitiesEntityFields.ADMIN_E2_E_BY_DEFAULT, true) 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) { context: Context) {
companion object { companion object {
const val SESSION_STORE_SCHEMA_VERSION = 2L const val SESSION_STORE_SCHEMA_VERSION = 3L
} }
// Keep legacy preferences name for compatibility reason // Keep legacy preferences name for compatibility reason

View File

@ -31,7 +31,8 @@ internal object HomeServerCapabilitiesMapper {
maxUploadFileSize = entity.maxUploadFileSize, maxUploadFileSize = entity.maxUploadFileSize,
lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported, lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported,
defaultIdentityServerUrl = entity.defaultIdentityServerUrl, 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 lastVersionIdentityServerSupported: Boolean = false,
var defaultIdentityServerUrl: String? = null, var defaultIdentityServerUrl: String? = null,
var adminE2EByDefault: Boolean = true, var adminE2EByDefault: Boolean = true,
var lastUpdatedTimestamp: Long = 0L var lastUpdatedTimestamp: Long = 0L,
var preferredJitsiDomain: String? = null
) : RealmObject() { ) : RealmObject() {
companion object companion object

View File

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

View File

@ -110,6 +110,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) { if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) {
homeServerCapabilitiesEntity.defaultIdentityServerUrl = getWellknownResult.identityServerUrl homeServerCapabilitiesEntity.defaultIdentityServerUrl = getWellknownResult.identityServerUrl
homeServerCapabilitiesEntity.adminE2EByDefault = getWellknownResult.wellKnown.e2eAdminSetting?.e2eDefault ?: true homeServerCapabilitiesEntity.adminE2EByDefault = getWellknownResult.wellKnown.e2eAdminSetting?.e2eDefault ?: true
homeServerCapabilitiesEntity.preferredJitsiDomain = getWellknownResult.wellKnown.jitsiServer?.preferredDomain
// We are also checking for integration manager configurations // We are also checking for integration manager configurations
val config = configExtractor.extract(getWellknownResult.wellKnown) val config = configExtractor.extract(getWellknownResult.wellKnown)
if (config != null) { 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' implementation 'com.github.BillCarsonFr:JsonViewer:0.5'
// TODO meant for development purposes only // WebRTC
implementation 'org.webrtc:google-webrtc:1.0.+' // 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 // QR-code
// Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170 // 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="Recycle" severity="error" />
<issue id="KotlinPropertyAccess" severity="error" /> <issue id="KotlinPropertyAccess" severity="error" />
<!-- Ignore error from HtmlCompressor lib -->
<issue id="InvalidPackage"> <issue id="InvalidPackage">
<!-- Ignore error from HtmlCompressor lib -->
<ignore path="**/htmlcompressor-1.4.jar" /> <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> </issue>
<!-- Manifest --> <!-- Manifest -->

View File

@ -24,3 +24,44 @@
## print all the rules in a file ## print all the rules in a file
# -printconfiguration ../proguard_files/full-r8-config.txt # -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 --> <!-- Needed for incoming calls -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <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 --> <!-- Adding CAMERA permission prevents Chromebooks to see the application on the PlayStore -->
<!-- Tell that the Camera is not mandatory to install the application --> <!-- Tell that the Camera is not mandatory to install the application -->
<uses-feature <uses-feature
@ -202,6 +210,9 @@
android:name="im.vector.app.features.attachments.preview.AttachmentsPreviewActivity" android:name="im.vector.app.features.attachments.preview.AttachmentsPreviewActivity"
android:theme="@style/AppTheme.AttachmentsPreview" /> android:theme="@style/AppTheme.AttachmentsPreview" />
<activity android:name="im.vector.app.features.call.VectorCallActivity" /> <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.terms.ReviewTermsActivity" />
<activity android:name="im.vector.app.features.widgets.WidgetActivity" /> <activity android:name="im.vector.app.features.widgets.WidgetActivity" />

View File

@ -21,9 +21,6 @@
div { div {
padding: 4px; padding: 4px;
} }
</style> </style>
</head> </head>
<body> <body>
@ -392,6 +389,13 @@ SOFTWARE.
<li> <li>
<b>Copyright (C) 2018 stfalcon.com</b> <b>Copyright (C) 2018 stfalcon.com</b>
</li> </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> </ul>
<pre> <pre>
Apache License 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.MainActivity
import im.vector.app.features.call.CallControlsBottomSheet import im.vector.app.features.call.CallControlsBottomSheet
import im.vector.app.features.call.VectorCallActivity 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.createdirect.CreateDirectRoomActivity
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
@ -140,6 +141,7 @@ interface ScreenComponent {
fun inject(activity: WidgetActivity) fun inject(activity: WidgetActivity)
fun inject(activity: VectorCallActivity) fun inject(activity: VectorCallActivity)
fun inject(activity: VectorAttachmentViewerActivity) fun inject(activity: VectorAttachmentViewerActivity)
fun inject(activity: VectorJitsiActivity)
/* ========================================================================================== /* ==========================================================================================
* BottomSheets * BottomSheets

View File

@ -36,7 +36,7 @@ abstract class GenericButtonItem : VectorEpoxyModel<GenericButtonItem.Holder>()
var text: String? = null var text: String? = null
@EpoxyAttribute @EpoxyAttribute
var itemClickAction: View.OnClickListener? = null var buttonClickAction: View.OnClickListener? = null
@EpoxyAttribute @EpoxyAttribute
@ColorInt @ColorInt
@ -57,7 +57,7 @@ abstract class GenericButtonItem : VectorEpoxyModel<GenericButtonItem.Holder>()
holder.button.icon = null holder.button.icon = null
} }
itemClickAction?.let { holder.view.setOnClickListener(it) } buttonClickAction?.let { holder.button.setOnClickListener(it) }
} }
class Holder : VectorEpoxyHolder() { 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.model.message.MessageWithAttachmentContent
import org.matrix.android.sdk.api.session.room.timeline.Timeline 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.TimelineEvent
import org.matrix.android.sdk.api.session.widgets.model.Widget
sealed class RoomDetailAction : VectorViewModelAction { sealed class RoomDetailAction : VectorViewModelAction {
data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction() data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction()
@ -80,4 +81,10 @@ sealed class RoomDetailAction : VectorViewModelAction {
object SelectStickerAttachment : RoomDetailAction() object SelectStickerAttachment : RoomDetailAction()
object OpenIntegrationManager: 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.RequireActiveMembershipViewEvents
import im.vector.app.features.room.RequireActiveMembershipViewModel import im.vector.app.features.room.RequireActiveMembershipViewModel
import im.vector.app.features.room.RequireActiveMembershipViewState 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.activity_room_detail.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import javax.inject.Inject import javax.inject.Inject
@ -42,7 +44,8 @@ import javax.inject.Inject
class RoomDetailActivity : class RoomDetailActivity :
VectorBaseActivity(), VectorBaseActivity(),
ToolbarConfigurable, ToolbarConfigurable,
RequireActiveMembershipViewModel.Factory { RequireActiveMembershipViewModel.Factory,
RoomWidgetPermissionViewModel.Factory {
override fun getLayoutRes() = R.layout.activity_room_detail override fun getLayoutRes() = R.layout.activity_room_detail
@ -57,6 +60,12 @@ class RoomDetailActivity :
return requireActiveMembershipViewModelFactory.create(initialState.copy(roomId = currentRoomId ?: "")) 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) { override fun injectWith(injector: ScreenComponent) {
super.injectWith(injector) super.injectWith(injector)
injector.inject(this) injector.inject(this)

View File

@ -29,8 +29,11 @@ import android.text.Spannable
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes 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.resources.ColorProvider
import im.vector.app.core.ui.views.ActiveCallView import im.vector.app.core.ui.views.ActiveCallView
import im.vector.app.core.ui.views.ActiveCallViewHolder 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.JumpToReadMarkerView
import im.vector.app.core.ui.views.NotificationAreaView import im.vector.app.core.ui.views.NotificationAreaView
import im.vector.app.core.utils.Debouncer 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.SharedActiveCallViewModel
import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.WebRtcPeerConnectionManager 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.command.Command
import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
import im.vector.app.features.crypto.util.toImageRes 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.MessageTextItem
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData 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.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.RoomWidgetsBottomSheet
import im.vector.app.features.home.room.detail.widget.WidgetRequestCodes import im.vector.app.features.home.room.detail.widget.WidgetRequestCodes
import im.vector.app.features.html.EventHtmlRenderer 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.share.SharedData
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.widgets.WidgetActivity 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.MatrixCallback
import org.matrix.android.sdk.api.permalinks.PermalinkFactory import org.matrix.android.sdk.api.permalinks.PermalinkFactory
import org.matrix.android.sdk.api.session.Session 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.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent 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.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.session.widgets.model.WidgetType
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt 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.EncryptedEventContent
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode 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 timber.log.Timber
import java.io.File import java.io.File
import java.net.URL import java.net.URL
@ -217,7 +225,7 @@ class RoomDetailFragment @Inject constructor(
JumpToReadMarkerView.Callback, JumpToReadMarkerView.Callback,
AttachmentTypeSelectorView.Callback, AttachmentTypeSelectorView.Callback,
AttachmentsHelper.Callback, AttachmentsHelper.Callback,
RoomWidgetsBannerView.Callback, // RoomWidgetsBannerView.Callback,
ActiveCallView.Callback { ActiveCallView.Callback {
companion object { companion object {
@ -292,7 +300,7 @@ class RoomDetailFragment @Inject constructor(
setupJumpToReadMarkerView() setupJumpToReadMarkerView()
setupActiveCallView() setupActiveCallView()
setupJumpToBottomView() setupJumpToBottomView()
setupWidgetsBannerView() setupConfBannerView()
roomToolbarContentView.debouncedClicks { roomToolbarContentView.debouncedClicks {
navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId) navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
@ -350,10 +358,44 @@ class RoomDetailFragment @Inject constructor(
is RoomDetailViewEvents.DisplayEnableIntegrationsWarning -> displayDisabledIntegrationDialog() is RoomDetailViewEvents.DisplayEnableIntegrationsWarning -> displayDisabledIntegrationDialog()
is RoomDetailViewEvents.OpenIntegrationManager -> openIntegrationManager() is RoomDetailViewEvents.OpenIntegrationManager -> openIntegrationManager()
is RoomDetailViewEvents.OpenFile -> startOpenFileIntent(it) 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 }.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) { private fun openIntegrationManager(screen: String? = null) {
navigator.openIntegrationManager( navigator.openIntegrationManager(
fragment = this, fragment = this,
@ -363,8 +405,33 @@ class RoomDetailFragment @Inject constructor(
) )
} }
private fun setupWidgetsBannerView() { private fun setupConfBannerView() {
roomWidgetsBannerView.callback = this 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) { 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) { override fun onPrepareOptionsMenu(menu: Menu) {
menu.forEach { menu.forEach {
it.isVisible = roomDetailViewModel.isMenuItemVisible(it.itemId) 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 { override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -549,26 +646,12 @@ class RoomDetailFragment @Inject constructor(
true true
} }
R.id.open_matrix_apps -> { R.id.open_matrix_apps -> {
roomDetailViewModel.handle(RoomDetailAction.OpenIntegrationManager) roomDetailViewModel.handle(RoomDetailAction.ManageIntegrations)
true true
} }
R.id.voice_call, R.id.voice_call,
R.id.video_call -> { R.id.video_call -> {
val activeCall = sharedCallActionViewModel.activeCall.value handleCallRequest(item)
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)
}
true true
} }
R.id.hangup_call -> { 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() { private fun displayDisabledIntegrationDialog() {
AlertDialog.Builder(requireActivity()) AlertDialog.Builder(requireActivity())
.setTitle(R.string.disabled_integration_dialog_title) .setTitle(R.string.disabled_integration_dialog_title)
@ -871,9 +1010,9 @@ class RoomDetailFragment @Inject constructor(
invalidateOptionsMenu() invalidateOptionsMenu()
val summary = state.asyncRoomSummary() val summary = state.asyncRoomSummary()
renderToolbar(summary, state.typingMessage) renderToolbar(summary, state.typingMessage)
activeConferenceView.render(state)
val inviter = state.asyncInviter() val inviter = state.asyncInviter()
if (summary?.membership == Membership.JOIN) { if (summary?.membership == Membership.JOIN) {
roomWidgetsBannerView.render(state.activeRoomWidgets())
jumpToBottomView.count = summary.notificationCount jumpToBottomView.count = summary.notificationCount
jumpToBottomView.drawBadge = summary.hasUnreadMessages jumpToBottomView.drawBadge = summary.hasUnreadMessages
scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline
@ -1111,7 +1250,7 @@ class RoomDetailFragment @Inject constructor(
} }
} }
// TimelineEventController.Callback ************************************************************ // TimelineEventController.Callback ************************************************************
override fun onUrlClicked(url: String, title: String): Boolean { override fun onUrlClicked(url: String, title: String): Boolean {
permalinkHandler permalinkHandler
@ -1583,7 +1722,14 @@ class RoomDetailFragment @Inject constructor(
Snackbar.make(requireView(), message, duration).show() 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() { override fun onAcceptInvite() {
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
@ -1595,7 +1741,7 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(RoomDetailAction.RejectInvite) roomDetailViewModel.handle(RoomDetailAction.RejectInvite)
} }
// JumpToReadMarkerView.Callback // JumpToReadMarkerView.Callback
override fun onJumpToReadMarkerClicked() = withState(roomDetailViewModel) { override fun onJumpToReadMarkerClicked() = withState(roomDetailViewModel) {
jumpToReadMarkerView.isVisible = false jumpToReadMarkerView.isVisible = false
@ -1611,7 +1757,7 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(RoomDetailAction.MarkAllAsRead) roomDetailViewModel.handle(RoomDetailAction.MarkAllAsRead)
} }
// AttachmentTypeSelectorView.Callback // AttachmentTypeSelectorView.Callback
override fun onTypeSelected(type: AttachmentTypeSelectorView.Type) { override fun onTypeSelected(type: AttachmentTypeSelectorView.Type) {
if (checkPermissions(type.permissionsBit, this, PERMISSION_REQUEST_CODE_PICK_ATTACHMENT)) { if (checkPermissions(type.permissionsBit, this, PERMISSION_REQUEST_CODE_PICK_ATTACHMENT)) {
@ -1632,7 +1778,7 @@ class RoomDetailFragment @Inject constructor(
}.exhaustive }.exhaustive
} }
// AttachmentsHelper.Callback // AttachmentsHelper.Callback
override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) { override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) {
if (roomDetailViewModel.preventAttachmentPreview) { if (roomDetailViewModel.preventAttachmentPreview) {
@ -1662,7 +1808,7 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(RoomDetailAction.SendMessage(formattedContact, false)) roomDetailViewModel.handle(RoomDetailAction.SendMessage(formattedContact, false))
} }
override fun onViewWidgetsClicked() { private fun onViewWidgetsClicked() {
RoomWidgetsBottomSheet.newInstance() RoomWidgetsBottomSheet.newInstance()
.show(childFragmentManager, "ROOM_WIDGETS_BOTTOM_SHEET") .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 ActionFailure(val action: RoomDetailAction, val throwable: Throwable) : RoomDetailViewEvents()
data class ShowMessage(val message: String) : RoomDetailViewEvents() data class ShowMessage(val message: String) : RoomDetailViewEvents()
data class ShowInfoOkDialog(val message: String) : RoomDetailViewEvents()
data class ShowE2EErrorMessage(val withHeldCode: WithHeldCode?) : RoomDetailViewEvents() data class ShowE2EErrorMessage(val withHeldCode: WithHeldCode?) : RoomDetailViewEvents()
data class NavigateToEvent(val eventId: String) : 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( data class FileTooBigError(
val filename: String, val filename: String,
@ -66,6 +71,10 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
data class OpenStickerPicker(val widget: Widget): RoomDetailViewEvents() data class OpenStickerPicker(val widget: Widget): RoomDetailViewEvents()
object OpenIntegrationManager: RoomDetailViewEvents() object OpenIntegrationManager: RoomDetailViewEvents()
object OpenActiveWidgetBottomSheet: RoomDetailViewEvents()
data class RequestNativeWidgetPermission(val widget: Widget,
val domain: String,
val grantedEvents: RoomDetailViewEvents) : RoomDetailViewEvents()
object MessageSent : SendMessageResult() object MessageSent : SendMessageResult()
data class JoinRoomCommandSuccess(val roomId: String) : 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.detail.timeline.helper.TimelineDisplayableEvents
import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import im.vector.app.features.settings.VectorLocale
import im.vector.app.features.settings.VectorPreferences 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.MatrixCallback
import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.NoOpMatrixCallback 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.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings 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.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.api.util.toOptional
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt 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.EncryptedEventContent
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode 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.rx
import org.matrix.android.sdk.rx.unwrap 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 timber.log.Timber
import java.io.File import java.io.File
import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -188,8 +193,12 @@ class RoomDetailViewModel @AssistedInject constructor(
PowerLevelsObservableFactory(room).createObservable() PowerLevelsObservableFactory(room).createObservable()
.subscribe { .subscribe {
val canSendMessage = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE) val canSendMessage = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE)
val isAllowedToManageWidgets = session.widgetService().hasPermissionsToHandleWidgets(room.roomId)
setState { setState {
copy(canSendMessage = canSendMessage) copy(
canSendMessage = canSendMessage,
isAllowedToManageWidgets = isAllowedToManageWidgets
)
} }
} }
.disposeOnClear() .disposeOnClear()
@ -269,6 +278,10 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
is RoomDetailAction.StartCall -> handleStartCall(action) is RoomDetailAction.StartCall -> handleStartCall(action)
is RoomDetailAction.EndCall -> handleEndCall() 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 }.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() { private fun startTrackingUnreadMessages() {
trackUnreadMessages.set(true) trackUnreadMessages.set(true)
setState { copy(canShowJumpToReadMarker = false) } setState { copy(canShowJumpToReadMarker = false) }
@ -419,7 +541,7 @@ class RoomDetailViewModel @AssistedInject constructor(
R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true
R.id.open_matrix_apps -> true R.id.open_matrix_apps -> true
R.id.voice_call, 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 R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null
else -> false else -> false
} }

View File

@ -66,7 +66,8 @@ data class RoomDetailViewState(
val unreadState: UnreadState = UnreadState.Unknown, val unreadState: UnreadState = UnreadState.Unknown,
val canShowJumpToReadMarker: Boolean = true, val canShowJumpToReadMarker: Boolean = true,
val changeMembershipState: ChangeMembershipState = ChangeMembershipState.Unknown, val changeMembershipState: ChangeMembershipState = ChangeMembershipState.Unknown,
val canSendMessage: Boolean = true val canSendMessage: Boolean = true,
val isAllowedToManageWidgets: Boolean = false
) : MvRxState { ) : MvRxState {
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) 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 package im.vector.app.features.home.room.detail.widget
import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder 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.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.onClick 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 org.matrix.android.sdk.api.session.widgets.model.Widget
import java.net.URL
@EpoxyModelClass(layout = R.layout.item_room_widget) @EpoxyModelClass(layout = R.layout.item_room_widget)
abstract class RoomWidgetItem : EpoxyModelWithHolder<RoomWidgetItem.Holder>() { abstract class RoomWidgetItem : EpoxyModelWithHolder<RoomWidgetItem.Holder>() {
@EpoxyAttribute lateinit var widget: Widget @EpoxyAttribute lateinit var widget: Widget
@EpoxyAttribute var widgetClicked: ClickListener? = null @EpoxyAttribute var widgetClicked: ClickListener? = null
@DrawableRes
@EpoxyAttribute var iconRes: Int? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.widgetName.text = widget.name 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) holder.view.onClick(widgetClicked)
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {
val widgetName by bind<TextView>(R.id.roomWidgetName) 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.extensions.configureWith
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.core.resources.ColorProvider 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.RoomDetailViewModel
import im.vector.app.features.home.room.detail.RoomDetailViewState import im.vector.app.features.home.room.detail.RoomDetailViewState
import im.vector.app.features.navigation.Navigator import im.vector.app.features.navigation.Navigator
@ -37,9 +38,9 @@ import javax.inject.Inject
/** /**
* Bottom sheet displaying active widgets in a room * 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 colorProvider: ColorProvider
@Inject lateinit var navigator: Navigator @Inject lateinit var navigator: Navigator
@ -77,6 +78,11 @@ class RoomWidgetsBottomSheet : VectorBaseBottomSheetDialogFragment(), RoomWidget
dismiss() dismiss()
} }
override fun didSelectManageWidgets() {
roomDetailViewModel.handle(RoomDetailAction.OpenIntegrationManager)
dismiss()
}
companion object { companion object {
fun newInstance(): RoomWidgetsBottomSheet { fun newInstance(): RoomWidgetsBottomSheet {
return 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.error.fatalError
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.toast 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.createdirect.CreateDirectRoomActivity
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity 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.room.model.thirdparty.RoomDirectoryData
import org.matrix.android.sdk.api.session.terms.TermsService 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.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.MatrixItem
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -270,9 +273,14 @@ class DefaultNavigator @Inject constructor(
fragment.startActivityForResult(intent, WidgetRequestCodes.INTEGRATION_MANAGER_REQUEST_CODE) fragment.startActivityForResult(intent, WidgetRequestCodes.INTEGRATION_MANAGER_REQUEST_CODE)
} }
override fun openRoomWidget(context: Context, roomId: String, widget: Widget) { override fun openRoomWidget(context: Context, roomId: String, widget: Widget, options: Map<String, Any>?) {
val widgetArgs = widgetArgsBuilder.buildRoomWidgetArgs(roomId, widget) if (widget.type is WidgetType.Jitsi) {
context.startActivity(WidgetActivity.newIntent(context, widgetArgs)) 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) { 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 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, fun openMediaViewer(activity: Activity,
roomId: String, roomId: String,

View File

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

View File

@ -48,6 +48,9 @@ class RoomWidgetPermissionBottomSheet : VectorBaseBottomSheetDialogFragment() {
injector.inject(this) 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 -> override fun invalidate() = withState(viewModel) { state ->
super.invalidate() super.invalidate()
val permissionData = state.permissionData() ?: return@withState val permissionData = state.permissionData() ?: return@withState
@ -88,6 +91,7 @@ class RoomWidgetPermissionBottomSheet : VectorBaseBottomSheetDialogFragment() {
@OnClick(R.id.widgetPermissionDecline) @OnClick(R.id.widgetPermissionDecline)
fun doDecline() { fun doDecline() {
viewModel.handle(RoomWidgetPermissionActions.BlockWidget) viewModel.handle(RoomWidgetPermissionActions.BlockWidget)
directListener?.invoke(false)
// optimistic dismiss // optimistic dismiss
dismiss() dismiss()
} }
@ -95,6 +99,7 @@ class RoomWidgetPermissionBottomSheet : VectorBaseBottomSheetDialogFragment() {
@OnClick(R.id.widgetPermissionContinue) @OnClick(R.id.widgetPermissionContinue)
fun doAccept() { fun doAccept() {
viewModel.handle(RoomWidgetPermissionActions.AllowWidget) viewModel.handle(RoomWidgetPermissionActions.AllowWidget)
directListener?.invoke(true)
// optimistic dismiss // optimistic dismiss
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.R
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import kotlinx.coroutines.launch 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 org.matrix.android.sdk.rx.rx
import timber.log.Timber import timber.log.Timber
import java.net.URL 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? // TODO check from widget urls the perms that should be shown?
// For now put all // For now put all
val infoShared = listOf( if (widget.type == WidgetType.Jitsi) {
R.string.room_widget_permission_display_name, val infoShared = listOf(
R.string.room_widget_permission_avatar_url, R.string.room_widget_permission_display_name,
R.string.room_widget_permission_user_id, R.string.room_widget_permission_avatar_url
R.string.room_widget_permission_theme, )
R.string.room_widget_permission_widget_id, RoomWidgetPermissionViewState.WidgetPermissionData(
R.string.room_widget_permission_room_id widget = widget,
) isWebviewWidget = false,
RoomWidgetPermissionViewState.WidgetPermissionData( permissionsList = infoShared,
widget = widget, widgetDomain = widget.widgetContent.data["domain"] as? String
isWebviewWidget = true, )
permissionsList = infoShared, } else {
widgetDomain = domain 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 { .execute {
copy(permissionData = it) copy(permissionData = it)
@ -91,7 +106,14 @@ class RoomWidgetPermissionViewModel @AssistedInject constructor(@Assisted val in
if (state.permissionData()?.isWebviewWidget.orFalse()) { if (state.permissionData()?.isWebviewWidget.orFalse()) {
WidgetPermissionsHelper(integrationManagerService, widgetService).changePermission(state.roomId, widgetId, false) WidgetPermissionsHelper(integrationManagerService, widgetService).changePermission(state.roomId, widgetId, false)
} else { } else {
// TODO JITSI awaitCallback<Unit> {
session.integrationManagerService().setNativeWidgetDomainAllowed(
state.permissionData.invoke()?.widget?.type?.preferred ?: "",
state.permissionData.invoke()?.widgetDomain ?: "",
false,
it
)
}
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.v("Failure revoking widget: ${state.widgetId}") Timber.v("Failure revoking widget: ${state.widgetId}")
@ -109,7 +131,14 @@ class RoomWidgetPermissionViewModel @AssistedInject constructor(@Assisted val in
if (state.permissionData()?.isWebviewWidget.orFalse()) { if (state.permissionData()?.isWebviewWidget.orFalse()) {
WidgetPermissionsHelper(integrationManagerService, widgetService).changePermission(state.roomId, widgetId, true) WidgetPermissionsHelper(integrationManagerService, widgetService).changePermission(state.roomId, widgetId, true)
} else { } else {
// TODO JITSI awaitCallback<Unit> {
session.integrationManagerService().setNativeWidgetDomainAllowed(
state.permissionData.invoke()?.widget?.type?.preferred ?: "",
state.permissionData.invoke()?.widgetDomain ?: "",
true,
it
)
}
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.v("Failure allowing widget: ${state.widgetId}") 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" app:layout_constraintTop_toBottomOf="@id/syncStateView"
tools:visibility="visible" /> 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 <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
android:layout_width="0dp" android:layout_width="0dp"
@ -112,7 +120,7 @@
app:layout_constraintBottom_toTopOf="@+id/recyclerViewBarrier" app:layout_constraintBottom_toTopOf="@+id/recyclerViewBarrier"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="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" /> tools:listitem="@layout/item_timeline_event_base" />
<FrameLayout <FrameLayout
@ -121,17 +129,17 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="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 <!-- <im.vector.app.features.home.room.detail.widget.RoomWidgetsBannerView-->
android:id="@+id/roomWidgetsBannerView" <!-- android:id="@+id/roomWidgetsBannerView"-->
android:layout_width="match_parent" <!-- android:layout_width="match_parent"-->
android:layout_height="wrap_content" <!-- android:layout_height="wrap_content"-->
android:layout_marginStart="8dp" <!-- android:layout_marginStart="8dp"-->
android:layout_marginTop="8dp" <!-- android:layout_marginTop="8dp"-->
android:layout_marginEnd="8dp" <!-- android:layout_marginEnd="8dp"-->
android:visibility="gone" <!-- android:visibility="gone"-->
tools:visibility="visible" /> <!-- tools:visibility="visible" />-->
<im.vector.app.core.ui.views.JumpToReadMarkerView <im.vector.app.core.ui.views.JumpToReadMarkerView
android:id="@+id/jumpToReadMarkerView" android:id="@+id/jumpToReadMarkerView"
@ -190,7 +198,7 @@
android:focusable="true" android:focusable="true"
app:cardCornerRadius="16dp" app:cardCornerRadius="16dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/activeCallView"> app:layout_constraintTop_toBottomOf="@id/activeConferenceView">
<org.webrtc.SurfaceViewRenderer <org.webrtc.SurfaceViewRenderer
android:id="@+id/activeCallPiP" android:id="@+id/activeCallPiP"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout 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" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -16,11 +16,32 @@
android:layout_width="32dp" android:layout_width="32dp"
android:layout_height="32dp" android:layout_height="32dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" /> tools:src="@tools:sample/avatars" />
<TextView <TextView
android:id="@+id/roomWidgetName" android:id="@+id/roomWidgetName"
style="@style/BottomSheetItemTextMain" 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" android:textColor="@color/white"
app:drawableTint="@color/white" /> app:drawableTint="@color/white" />
<TextView <com.google.android.material.button.MaterialButton
android:id="@+id/returnToCallButton" android:id="@+id/returnToCallButton"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignTop="@+id/activeCallInfo" android:layout_alignTop="@+id/activeCallInfo"
@ -38,7 +39,6 @@
android:paddingStart="8dp" android:paddingStart="8dp"
android:paddingEnd="16dp" android:paddingEnd="16dp"
android:text="@string/return_to_call" android:text="@string/return_to_call"
android:textAllCaps="true"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="15sp" android:textSize="15sp"
android:textStyle="bold" /> 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" app:showAsAction="always"
tools:visible="true" /> tools:visible="true" />
<item
android:id="@+id/open_matrix_apps"
android:title="@string/room_add_matrix_apps"
app:showAsAction="never" />
<item <item
android:id="@+id/voice_call" android:id="@+id/voice_call"
android:icon="@drawable/ic_phone" android:icon="@drawable/ic_phone"
@ -35,6 +30,12 @@
app:showAsAction="always" app:showAsAction="always"
tools:visible="true" /> 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 <item
android:id="@+id/resend_all" android:id="@+id/resend_all"
android:icon="@drawable/ic_refresh_cw" android:icon="@drawable/ic_refresh_cw"

View File

@ -18,6 +18,9 @@
<!-- Note: pusher_app_id cannot exceed 64 chars --> <!-- Note: pusher_app_id cannot exceed 64 chars -->
<string name="pusher_app_id" translatable="false">im.vector.app.android</string> <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"> <string-array name="room_directory_servers" translatable="false">
<item>matrix.org</item> <item>matrix.org</item>
</string-array> </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_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_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="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="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="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="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="call_anyway">Call Anyway</string>
<string name="send_anyway">Send 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="widget_integration_invalid_parameter">A parameter is not valid.</string>
<string name="integration_manager_not_configured">No integration manager configured.</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_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">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_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> <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> <item name="android:textSize">16sp</item>
</style> </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"> <style name="BottomSheetItemTime">
<item name="android:layout_width">wrap_content</item> <item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>