mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-03-18 20:29:10 +03:00
Merge pull request #1914 from vector-im/feature/jitsi_native
Feature/jitsi native
This commit is contained in:
commit
550dcde9b8
54 changed files with 1480 additions and 158 deletions
1
.idea/dictionaries/bmarty.xml
generated
1
.idea/dictionaries/bmarty.xml
generated
|
@ -14,6 +14,7 @@
|
|||
<w>gplay</w>
|
||||
<w>hmac</w>
|
||||
<w>homeserver</w>
|
||||
<w>jitsi</w>
|
||||
<w>ktlint</w>
|
||||
<w>linkified</w>
|
||||
<w>linkify</w>
|
||||
|
|
|
@ -3,6 +3,7 @@ Changes in Element 1.0.5 (2020-XX-XX)
|
|||
|
||||
Features ✨:
|
||||
- Protect access to the app by a pin code (#1700)
|
||||
- Conference with Jitsi support (#43)
|
||||
|
||||
Improvements 🙌:
|
||||
- Give user the possibility to prevent accidental call (#1869)
|
||||
|
|
|
@ -52,6 +52,12 @@ allprojects {
|
|||
}
|
||||
}
|
||||
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
|
||||
// Jitsi repo
|
||||
maven {
|
||||
url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-2.9.3"
|
||||
// Note: to test Jitsi release you can use a local file like this:
|
||||
// url "file:///Users/bmarty/workspaces/jitsi_libre_maven/android-sdk-2.9.3"
|
||||
}
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
|
82
docs/jitsi.md
Normal file
82
docs/jitsi.md
Normal 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.
|
|
@ -132,8 +132,13 @@ dependencies {
|
|||
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||
implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
|
||||
implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version"
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.2.2'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
|
||||
|
||||
|
||||
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.8.1"))
|
||||
implementation 'com.squareup.okhttp3:okhttp'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor'
|
||||
implementation 'com.squareup.okhttp3:okhttp-urlconnection'
|
||||
|
||||
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
|
||||
|
||||
|
@ -174,8 +179,10 @@ dependencies {
|
|||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23'
|
||||
|
||||
// Web RTC
|
||||
// TODO meant for development purposes only. See http://webrtc.github.io/webrtc-org/native-code/android/
|
||||
implementation 'org.webrtc:google-webrtc:1.0.+'
|
||||
// org.webrtc:google-webrtc is for development purposes only. See http://webrtc.github.io/webrtc-org/native-code/android/
|
||||
// implementation 'org.webrtc:google-webrtc:1.0.+'
|
||||
// Use the same WebRTC library than the one used by Jitsi library
|
||||
implementation('com.facebook.react:react-native-webrtc:1.84.0-jitsi-5112273@aar')
|
||||
|
||||
debugImplementation 'com.airbnb.okreplay:okreplay:1.5.0'
|
||||
releaseImplementation 'com.airbnb.okreplay:noop:1.5.0'
|
||||
|
|
|
@ -42,6 +42,9 @@ import org.matrix.android.sdk.api.util.JsonDict
|
|||
* }
|
||||
* ]
|
||||
* }
|
||||
* "im.vector.riot.jitsi": {
|
||||
* "preferredDomain": "https://jitsi.riot.im/"
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
|
@ -57,7 +60,10 @@ data class WellKnown(
|
|||
val integrations: JsonDict? = null,
|
||||
|
||||
@Json(name = "im.vector.riot.e2ee")
|
||||
val e2eAdminSetting: E2EWellKnownConfig? = null
|
||||
val e2eAdminSetting: E2EWellKnownConfig? = null,
|
||||
|
||||
@Json(name = "im.vector.riot.jitsi")
|
||||
val jitsiServer: WellKnownPreferredConfig? = null
|
||||
|
||||
)
|
||||
|
||||
|
@ -66,3 +72,9 @@ data class E2EWellKnownConfig(
|
|||
@Json(name = "default")
|
||||
val e2eDefault: Boolean = true
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class WellKnownPreferredConfig(
|
||||
@Json(name = "preferredDomain")
|
||||
val preferredDomain: String? = null
|
||||
)
|
||||
|
|
|
@ -38,7 +38,11 @@ data class HomeServerCapabilities(
|
|||
* Option to allow homeserver admins to set the default E2EE behaviour back to disabled for DMs / private rooms
|
||||
* (as it was before) for various environments where this is desired.
|
||||
*/
|
||||
val adminE2EByDefault: Boolean = true
|
||||
val adminE2EByDefault: Boolean = true,
|
||||
/**
|
||||
* Preferred Jitsi domain, provided in Wellknown
|
||||
*/
|
||||
val preferredJitsiDomain: String? = null
|
||||
) {
|
||||
companion object {
|
||||
const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L
|
||||
|
|
|
@ -17,10 +17,10 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.database
|
||||
|
||||
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
|
||||
import io.realm.DynamicRealm
|
||||
import io.realm.RealmMigration
|
||||
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -31,6 +31,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
|||
|
||||
if (oldVersion <= 0) migrateTo1(realm)
|
||||
if (oldVersion <= 1) migrateTo2(realm)
|
||||
if (oldVersion <= 2) migrateTo3(realm)
|
||||
}
|
||||
|
||||
private fun migrateTo1(realm: DynamicRealm) {
|
||||
|
@ -52,4 +53,14 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
|||
obj.setBoolean(HomeServerCapabilitiesEntityFields.ADMIN_E2_E_BY_DEFAULT, true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrateTo3(realm: DynamicRealm) {
|
||||
Timber.d("Step 2 -> 3")
|
||||
realm.schema.get("HomeServerCapabilitiesEntity")
|
||||
?.addField(HomeServerCapabilitiesEntityFields.PREFERRED_JITSI_DOMAIN, String::class.java)
|
||||
?.transform { obj ->
|
||||
// Schedule a refresh of the capabilities
|
||||
obj.setLong(HomeServerCapabilitiesEntityFields.LAST_UPDATED_TIMESTAMP, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(
|
|||
context: Context) {
|
||||
|
||||
companion object {
|
||||
const val SESSION_STORE_SCHEMA_VERSION = 2L
|
||||
const val SESSION_STORE_SCHEMA_VERSION = 3L
|
||||
}
|
||||
|
||||
// Keep legacy preferences name for compatibility reason
|
||||
|
|
|
@ -31,7 +31,8 @@ internal object HomeServerCapabilitiesMapper {
|
|||
maxUploadFileSize = entity.maxUploadFileSize,
|
||||
lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported,
|
||||
defaultIdentityServerUrl = entity.defaultIdentityServerUrl,
|
||||
adminE2EByDefault = entity.adminE2EByDefault
|
||||
adminE2EByDefault = entity.adminE2EByDefault,
|
||||
preferredJitsiDomain = entity.preferredJitsiDomain
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,8 @@ internal open class HomeServerCapabilitiesEntity(
|
|||
var lastVersionIdentityServerSupported: Boolean = false,
|
||||
var defaultIdentityServerUrl: String? = null,
|
||||
var adminE2EByDefault: Boolean = true,
|
||||
var lastUpdatedTimestamp: Long = 0L
|
||||
var lastUpdatedTimestamp: Long = 0L,
|
||||
var preferredJitsiDomain: String? = null
|
||||
) : RealmObject() {
|
||||
|
||||
companion object
|
||||
|
|
|
@ -77,7 +77,11 @@ internal object NetworkModule {
|
|||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.writeTimeout(60, TimeUnit.SECONDS)
|
||||
.addNetworkInterceptor(stethoInterceptor)
|
||||
.apply {
|
||||
if (BuildConfig.DEBUG) {
|
||||
addNetworkInterceptor(stethoInterceptor)
|
||||
}
|
||||
}
|
||||
.addInterceptor(timeoutInterceptor)
|
||||
.addInterceptor(userAgentInterceptor)
|
||||
.addInterceptor(httpLoggingInterceptor)
|
||||
|
|
|
@ -110,6 +110,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
|||
if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) {
|
||||
homeServerCapabilitiesEntity.defaultIdentityServerUrl = getWellknownResult.identityServerUrl
|
||||
homeServerCapabilitiesEntity.adminE2EByDefault = getWellknownResult.wellKnown.e2eAdminSetting?.e2eDefault ?: true
|
||||
homeServerCapabilitiesEntity.preferredJitsiDomain = getWellknownResult.wellKnown.jitsiServer?.preferredDomain
|
||||
// We are also checking for integration manager configurations
|
||||
val config = configExtractor.extract(getWellknownResult.wellKnown)
|
||||
if (config != null) {
|
||||
|
|
65
tools/jitsi/build_jisti_libs.sh
Executable file
65
tools/jitsi/build_jisti_libs.sh
Executable 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 "##################################################"
|
|
@ -405,8 +405,10 @@ dependencies {
|
|||
|
||||
implementation 'com.github.BillCarsonFr:JsonViewer:0.5'
|
||||
|
||||
// TODO meant for development purposes only
|
||||
implementation 'org.webrtc:google-webrtc:1.0.+'
|
||||
// WebRTC
|
||||
// org.webrtc:google-webrtc is for development purposes only
|
||||
// implementation 'org.webrtc:google-webrtc:1.0.+'
|
||||
implementation('org.jitsi.react:jitsi-meet-sdk:2.9.3') { transitive = true }
|
||||
|
||||
// QR-code
|
||||
// Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170
|
||||
|
|
|
@ -40,9 +40,11 @@
|
|||
<issue id="Recycle" severity="error" />
|
||||
<issue id="KotlinPropertyAccess" severity="error" />
|
||||
|
||||
<!-- Ignore error from HtmlCompressor lib -->
|
||||
<issue id="InvalidPackage">
|
||||
<!-- Ignore error from HtmlCompressor lib -->
|
||||
<ignore path="**/htmlcompressor-1.4.jar" />
|
||||
<!-- Ignore error from dropbox-core-sdk-3.0.8 lib, which comes with Jitsi library -->
|
||||
<ignore path="**/dropbox-core-sdk-3.0.8.jar" />
|
||||
</issue>
|
||||
|
||||
<!-- Manifest -->
|
||||
|
|
41
vector/proguard-rules.pro
vendored
41
vector/proguard-rules.pro
vendored
|
@ -24,3 +24,44 @@
|
|||
|
||||
## print all the rules in a file
|
||||
# -printconfiguration ../proguard_files/full-r8-config.txt
|
||||
|
||||
# WebRTC
|
||||
|
||||
-keep class org.webrtc.** { *; }
|
||||
-dontwarn org.chromium.build.BuildHooksAndroid
|
||||
|
||||
# Jitsi (else callbacks are not called)
|
||||
|
||||
-keep class org.jitsi.meet.** { *; }
|
||||
-keep class org.jitsi.meet.sdk.** { *; }
|
||||
|
||||
# React Native
|
||||
|
||||
# Keep our interfaces so they can be used by other ProGuard rules.
|
||||
# See http://sourceforge.net/p/proguard/bugs/466/
|
||||
-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip
|
||||
-keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters
|
||||
-keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip
|
||||
|
||||
# Do not strip any method/class that is annotated with @DoNotStrip
|
||||
-keep @com.facebook.proguard.annotations.DoNotStrip class *
|
||||
-keep @com.facebook.common.internal.DoNotStrip class *
|
||||
-keepclassmembers class * {
|
||||
@com.facebook.proguard.annotations.DoNotStrip *;
|
||||
@com.facebook.common.internal.DoNotStrip *;
|
||||
}
|
||||
|
||||
-keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * {
|
||||
void set*(***);
|
||||
*** get*();
|
||||
}
|
||||
|
||||
-keep class * extends com.facebook.react.bridge.JavaScriptModule { *; }
|
||||
-keep class * extends com.facebook.react.bridge.NativeModule { *; }
|
||||
-keepclassmembers,includedescriptorclasses class * { native <methods>; }
|
||||
-keepclassmembers class * { @com.facebook.react.uimanager.UIProp <fields>; }
|
||||
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp <methods>; }
|
||||
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup <methods>; }
|
||||
|
||||
-dontwarn com.facebook.react.**
|
||||
-keep,includedescriptorclasses class com.facebook.react.bridge.** { *; }
|
|
@ -28,6 +28,14 @@
|
|||
<!-- Needed for incoming calls -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<!-- Jitsi libs adds CALENDAR permissions, but we can remove them safely according to https://github.com/jitsi/jitsi-meet/issues/4068#issuecomment-480482481 -->
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_CALENDAR"
|
||||
tools:node="remove" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_CALENDAR"
|
||||
tools:node="remove" />
|
||||
|
||||
<!-- Adding CAMERA permission prevents Chromebooks to see the application on the PlayStore -->
|
||||
<!-- Tell that the Camera is not mandatory to install the application -->
|
||||
<uses-feature
|
||||
|
@ -202,6 +210,9 @@
|
|||
android:name="im.vector.app.features.attachments.preview.AttachmentsPreviewActivity"
|
||||
android:theme="@style/AppTheme.AttachmentsPreview" />
|
||||
<activity android:name="im.vector.app.features.call.VectorCallActivity" />
|
||||
<activity
|
||||
android:name="im.vector.app.features.call.conference.VectorJitsiActivity"
|
||||
android:configChanges="orientation|screenSize" />
|
||||
|
||||
<activity android:name="im.vector.app.features.terms.ReviewTermsActivity" />
|
||||
<activity android:name="im.vector.app.features.widgets.WidgetActivity" />
|
||||
|
|
|
@ -21,9 +21,6 @@
|
|||
div {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -392,6 +389,13 @@ SOFTWARE.
|
|||
<li>
|
||||
<b>Copyright (C) 2018 stfalcon.com</b>
|
||||
</li>
|
||||
<li>
|
||||
<b>Jitsi Meet (<a href="https://github.com/jitsi/jitsi-meet">jitsi-meet</a>)</b>
|
||||
<br/>
|
||||
Copyright @ 2020-present 8x8, Inc.
|
||||
<br/>
|
||||
Copyright @ 2017-2018 Atlassian Pty Ltd
|
||||
</li>
|
||||
</ul>
|
||||
<pre>
|
||||
Apache License
|
||||
|
|
|
@ -27,6 +27,7 @@ import im.vector.app.core.preference.UserAvatarPreference
|
|||
import im.vector.app.features.MainActivity
|
||||
import im.vector.app.features.call.CallControlsBottomSheet
|
||||
import im.vector.app.features.call.VectorCallActivity
|
||||
import im.vector.app.features.call.conference.VectorJitsiActivity
|
||||
import im.vector.app.features.createdirect.CreateDirectRoomActivity
|
||||
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
|
||||
|
@ -140,6 +141,7 @@ interface ScreenComponent {
|
|||
fun inject(activity: WidgetActivity)
|
||||
fun inject(activity: VectorCallActivity)
|
||||
fun inject(activity: VectorAttachmentViewerActivity)
|
||||
fun inject(activity: VectorJitsiActivity)
|
||||
|
||||
/* ==========================================================================================
|
||||
* BottomSheets
|
||||
|
|
|
@ -36,7 +36,7 @@ abstract class GenericButtonItem : VectorEpoxyModel<GenericButtonItem.Holder>()
|
|||
var text: String? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var itemClickAction: View.OnClickListener? = null
|
||||
var buttonClickAction: View.OnClickListener? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
@ColorInt
|
||||
|
@ -57,7 +57,7 @@ abstract class GenericButtonItem : VectorEpoxyModel<GenericButtonItem.Holder>()
|
|||
holder.button.icon = null
|
||||
}
|
||||
|
||||
itemClickAction?.let { holder.view.setOnClickListener(it) }
|
||||
buttonClickAction?.let { holder.button.setOnClickListener(it) }
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageStickerConte
|
|||
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||
|
||||
sealed class RoomDetailAction : VectorViewModelAction {
|
||||
data class UserIsTyping(val isTyping: Boolean) : RoomDetailAction()
|
||||
|
@ -80,4 +81,10 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
|||
|
||||
object SelectStickerAttachment : RoomDetailAction()
|
||||
object OpenIntegrationManager: RoomDetailAction()
|
||||
object ManageIntegrations: RoomDetailAction()
|
||||
data class AddJitsiWidget(val withVideo: Boolean): RoomDetailAction()
|
||||
data class RemoveWidget(val widgetId: String): RoomDetailAction()
|
||||
data class EnsureNativeWidgetAllowed(val widget: Widget,
|
||||
val userJustAccepted: Boolean,
|
||||
val grantedEvents: RoomDetailViewEvents) : RoomDetailAction()
|
||||
}
|
||||
|
|
|
@ -35,6 +35,8 @@ import im.vector.app.features.room.RequireActiveMembershipAction
|
|||
import im.vector.app.features.room.RequireActiveMembershipViewEvents
|
||||
import im.vector.app.features.room.RequireActiveMembershipViewModel
|
||||
import im.vector.app.features.room.RequireActiveMembershipViewState
|
||||
import im.vector.app.features.widgets.permissions.RoomWidgetPermissionViewModel
|
||||
import im.vector.app.features.widgets.permissions.RoomWidgetPermissionViewState
|
||||
import kotlinx.android.synthetic.main.activity_room_detail.*
|
||||
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
|
||||
import javax.inject.Inject
|
||||
|
@ -42,7 +44,8 @@ import javax.inject.Inject
|
|||
class RoomDetailActivity :
|
||||
VectorBaseActivity(),
|
||||
ToolbarConfigurable,
|
||||
RequireActiveMembershipViewModel.Factory {
|
||||
RequireActiveMembershipViewModel.Factory,
|
||||
RoomWidgetPermissionViewModel.Factory {
|
||||
|
||||
override fun getLayoutRes() = R.layout.activity_room_detail
|
||||
|
||||
|
@ -57,6 +60,12 @@ class RoomDetailActivity :
|
|||
return requireActiveMembershipViewModelFactory.create(initialState.copy(roomId = currentRoomId ?: ""))
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var permissionsViewModelFactory: RoomWidgetPermissionViewModel.Factory
|
||||
override fun create(initialState: RoomWidgetPermissionViewState): RoomWidgetPermissionViewModel {
|
||||
return permissionsViewModelFactory.create(initialState)
|
||||
}
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
super.injectWith(injector)
|
||||
injector.inject(this)
|
||||
|
|
|
@ -29,8 +29,11 @@ import android.text.Spannable
|
|||
import android.view.HapticFeedbackConstants
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
|
@ -77,6 +80,7 @@ import im.vector.app.core.platform.VectorBaseFragment
|
|||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.ui.views.ActiveCallView
|
||||
import im.vector.app.core.ui.views.ActiveCallViewHolder
|
||||
import im.vector.app.core.ui.views.ActiveConferenceView
|
||||
import im.vector.app.core.ui.views.JumpToReadMarkerView
|
||||
import im.vector.app.core.ui.views.NotificationAreaView
|
||||
import im.vector.app.core.utils.Debouncer
|
||||
|
@ -110,6 +114,7 @@ import im.vector.app.features.attachments.toGroupedContentAttachmentData
|
|||
import im.vector.app.features.call.SharedActiveCallViewModel
|
||||
import im.vector.app.features.call.VectorCallActivity
|
||||
import im.vector.app.features.call.WebRtcPeerConnectionManager
|
||||
import im.vector.app.features.call.conference.JitsiCallViewModel
|
||||
import im.vector.app.features.command.Command
|
||||
import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
|
||||
import im.vector.app.features.crypto.util.toImageRes
|
||||
|
@ -129,7 +134,6 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationD
|
|||
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
||||
import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
|
||||
import im.vector.app.features.home.room.detail.widget.RoomWidgetsBannerView
|
||||
import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet
|
||||
import im.vector.app.features.home.room.detail.widget.WidgetRequestCodes
|
||||
import im.vector.app.features.html.EventHtmlRenderer
|
||||
|
@ -147,6 +151,17 @@ import im.vector.app.features.settings.VectorSettingsActivity
|
|||
import im.vector.app.features.share.SharedData
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import im.vector.app.features.widgets.WidgetActivity
|
||||
import im.vector.app.features.widgets.WidgetArgs
|
||||
import im.vector.app.features.widgets.WidgetKind
|
||||
import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.fragment_room_detail.*
|
||||
import kotlinx.android.synthetic.main.merge_composer_layout.view.*
|
||||
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
|
||||
import org.billcarsonfr.jsonviewer.JSonViewerDialog
|
||||
import org.commonmark.parser.Parser
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.permalinks.PermalinkFactory
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
|
@ -169,20 +184,13 @@ import org.matrix.android.sdk.api.session.room.send.SendState
|
|||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.fragment_room_detail.*
|
||||
import kotlinx.android.synthetic.main.merge_composer_layout.view.*
|
||||
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
|
||||
import org.billcarsonfr.jsonviewer.JSonViewerDialog
|
||||
import org.commonmark.parser.Parser
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
|
@ -217,7 +225,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
JumpToReadMarkerView.Callback,
|
||||
AttachmentTypeSelectorView.Callback,
|
||||
AttachmentsHelper.Callback,
|
||||
RoomWidgetsBannerView.Callback,
|
||||
// RoomWidgetsBannerView.Callback,
|
||||
ActiveCallView.Callback {
|
||||
|
||||
companion object {
|
||||
|
@ -292,7 +300,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
setupJumpToReadMarkerView()
|
||||
setupActiveCallView()
|
||||
setupJumpToBottomView()
|
||||
setupWidgetsBannerView()
|
||||
setupConfBannerView()
|
||||
|
||||
roomToolbarContentView.debouncedClicks {
|
||||
navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
|
||||
|
@ -350,10 +358,44 @@ class RoomDetailFragment @Inject constructor(
|
|||
is RoomDetailViewEvents.DisplayEnableIntegrationsWarning -> displayDisabledIntegrationDialog()
|
||||
is RoomDetailViewEvents.OpenIntegrationManager -> openIntegrationManager()
|
||||
is RoomDetailViewEvents.OpenFile -> startOpenFileIntent(it)
|
||||
RoomDetailViewEvents.OpenActiveWidgetBottomSheet -> onViewWidgetsClicked()
|
||||
is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message)
|
||||
is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo)
|
||||
RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView()
|
||||
RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView()
|
||||
is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it)
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestNativeWidgetPermission(it: RoomDetailViewEvents.RequestNativeWidgetPermission) {
|
||||
val tag = RoomWidgetPermissionBottomSheet::class.java.name
|
||||
val dFrag = childFragmentManager.findFragmentByTag(tag) as? RoomWidgetPermissionBottomSheet
|
||||
if (dFrag != null && dFrag.dialog?.isShowing == true && !dFrag.isRemoving) {
|
||||
return
|
||||
} else {
|
||||
RoomWidgetPermissionBottomSheet.newInstance(
|
||||
WidgetArgs(
|
||||
baseUrl = it.domain,
|
||||
kind = WidgetKind.ROOM,
|
||||
roomId = roomDetailArgs.roomId,
|
||||
widgetId = it.widget.widgetId
|
||||
)
|
||||
).apply {
|
||||
directListener = { granted ->
|
||||
if (granted) {
|
||||
roomDetailViewModel.handle(RoomDetailAction.EnsureNativeWidgetAllowed(
|
||||
widget = it.widget,
|
||||
userJustAccepted = true,
|
||||
grantedEvents = it.grantedEvents
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
.show(childFragmentManager, tag)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openIntegrationManager(screen: String? = null) {
|
||||
navigator.openIntegrationManager(
|
||||
fragment = this,
|
||||
|
@ -363,8 +405,33 @@ class RoomDetailFragment @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun setupWidgetsBannerView() {
|
||||
roomWidgetsBannerView.callback = this
|
||||
private fun setupConfBannerView() {
|
||||
activeConferenceView.callback = object : ActiveConferenceView.Callback {
|
||||
override fun onTapJoinAudio(jitsiWidget: Widget) {
|
||||
// need to check if allowed first
|
||||
roomDetailViewModel.handle(RoomDetailAction.EnsureNativeWidgetAllowed(
|
||||
widget = jitsiWidget,
|
||||
userJustAccepted = false,
|
||||
grantedEvents = RoomDetailViewEvents.JoinJitsiConference(jitsiWidget, false))
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTapJoinVideo(jitsiWidget: Widget) {
|
||||
roomDetailViewModel.handle(RoomDetailAction.EnsureNativeWidgetAllowed(
|
||||
widget = jitsiWidget,
|
||||
userJustAccepted = false,
|
||||
grantedEvents = RoomDetailViewEvents.JoinJitsiConference(jitsiWidget, true))
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDelete(jitsiWidget: Widget) {
|
||||
roomDetailViewModel.handle(RoomDetailAction.RemoveWidget(jitsiWidget.widgetId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) {
|
||||
navigator.openRoomWidget(requireContext(), roomDetailArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to enableVideo))
|
||||
}
|
||||
|
||||
private fun openStickerPicker(event: RoomDetailViewEvents.OpenStickerPicker) {
|
||||
|
@ -529,10 +596,40 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
// We use a custom layout for this menu item, so we need to set a ClickListener
|
||||
menu.findItem(R.id.open_matrix_apps)?.let { menuItem ->
|
||||
menuItem.actionView.setOnClickListener {
|
||||
onOptionsItemSelected(menuItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
menu.forEach {
|
||||
it.isVisible = roomDetailViewModel.isMenuItemVisible(it.itemId)
|
||||
}
|
||||
withState(roomDetailViewModel) { state ->
|
||||
val matrixAppsMenuItem = menu.findItem(R.id.open_matrix_apps)
|
||||
val widgetsCount = state.activeRoomWidgets.invoke()?.size ?: 0
|
||||
if (widgetsCount > 0) {
|
||||
val actionView = matrixAppsMenuItem.actionView
|
||||
actionView
|
||||
.findViewById<ImageView>(R.id.action_view_icon_image)
|
||||
.setColorFilter(ContextCompat.getColor(requireContext(), R.color.riotx_accent))
|
||||
actionView.findViewById<TextView>(R.id.cart_badge).setTextOrHide("$widgetsCount")
|
||||
matrixAppsMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
||||
} else {
|
||||
// icon should be default color no badge
|
||||
val actionView = matrixAppsMenuItem.actionView
|
||||
actionView
|
||||
.findViewById<ImageView>(R.id.action_view_icon_image)
|
||||
.setColorFilter(ThemeUtils.getColor(requireContext(), R.attr.riotx_text_secondary))
|
||||
actionView.findViewById<TextView>(R.id.cart_badge).isVisible = false
|
||||
matrixAppsMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
@ -549,26 +646,12 @@ class RoomDetailFragment @Inject constructor(
|
|||
true
|
||||
}
|
||||
R.id.open_matrix_apps -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.OpenIntegrationManager)
|
||||
roomDetailViewModel.handle(RoomDetailAction.ManageIntegrations)
|
||||
true
|
||||
}
|
||||
R.id.voice_call,
|
||||
R.id.video_call -> {
|
||||
val activeCall = sharedCallActionViewModel.activeCall.value
|
||||
val isVideoCall = item.itemId == R.id.video_call
|
||||
if (activeCall != null) {
|
||||
// resume existing if same room, if not prompt to kill and then restart new call?
|
||||
if (activeCall.roomId == roomDetailArgs.roomId) {
|
||||
onTapToReturnToCall()
|
||||
}
|
||||
// else {
|
||||
// TODO might not work well, and should prompt
|
||||
// webRtcPeerConnectionManager.endCall()
|
||||
// safeStartCall(it, isVideoCall)
|
||||
// }
|
||||
} else {
|
||||
safeStartCall(isVideoCall)
|
||||
}
|
||||
handleCallRequest(item)
|
||||
true
|
||||
}
|
||||
R.id.hangup_call -> {
|
||||
|
@ -579,6 +662,62 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleCallRequest(item: MenuItem) = withState(roomDetailViewModel) { state ->
|
||||
val roomSummary = state.asyncRoomSummary.invoke() ?: return@withState
|
||||
val isVideoCall = item.itemId == R.id.video_call
|
||||
when (roomSummary.joinedMembersCount) {
|
||||
1 -> {
|
||||
val pendingInvite = roomSummary.invitedMembersCount ?: 0 > 0
|
||||
if (pendingInvite) {
|
||||
// wait for other to join
|
||||
showDialogWithMessage(getString(R.string.cannot_call_yourself_with_invite))
|
||||
} else {
|
||||
// You cannot place a call with yourself.
|
||||
showDialogWithMessage(getString(R.string.cannot_call_yourself))
|
||||
}
|
||||
}
|
||||
2 -> {
|
||||
val activeCall = sharedCallActionViewModel.activeCall.value
|
||||
if (activeCall != null) {
|
||||
// resume existing if same room, if not prompt to kill and then restart new call?
|
||||
if (activeCall.roomId == roomDetailArgs.roomId) {
|
||||
onTapToReturnToCall()
|
||||
}
|
||||
// else {
|
||||
// TODO might not work well, and should prompt
|
||||
// webRtcPeerConnectionManager.endCall()
|
||||
// safeStartCall(it, isVideoCall)
|
||||
// }
|
||||
} else {
|
||||
safeStartCall(isVideoCall)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// it's jitsi call
|
||||
// can you add widgets??
|
||||
if (!state.isAllowedToManageWidgets) {
|
||||
// You do not have permission to start a conference call in this room
|
||||
showDialogWithMessage(getString(R.string.no_permissions_to_start_conf_call))
|
||||
} else {
|
||||
if (state.activeRoomWidgets()?.filter { it.type == WidgetType.Jitsi }?.any() == true) {
|
||||
// A conference is already in progress!
|
||||
showDialogWithMessage(getString(R.string.conference_call_in_progress))
|
||||
} else {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(if (isVideoCall) R.string.video_meeting else R.string.audio_meeting)
|
||||
.setMessage(R.string.audio_video_meeting_description)
|
||||
.setPositiveButton(getString(R.string.create)) { _, _ ->
|
||||
// create the widget, then navigate to it..
|
||||
roomDetailViewModel.handle(RoomDetailAction.AddJitsiWidget(isVideoCall))
|
||||
}
|
||||
.setNegativeButton(getString(R.string.cancel), null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayDisabledIntegrationDialog() {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.disabled_integration_dialog_title)
|
||||
|
@ -871,9 +1010,9 @@ class RoomDetailFragment @Inject constructor(
|
|||
invalidateOptionsMenu()
|
||||
val summary = state.asyncRoomSummary()
|
||||
renderToolbar(summary, state.typingMessage)
|
||||
activeConferenceView.render(state)
|
||||
val inviter = state.asyncInviter()
|
||||
if (summary?.membership == Membership.JOIN) {
|
||||
roomWidgetsBannerView.render(state.activeRoomWidgets())
|
||||
jumpToBottomView.count = summary.notificationCount
|
||||
jumpToBottomView.drawBadge = summary.hasUnreadMessages
|
||||
scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline
|
||||
|
@ -1111,7 +1250,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// TimelineEventController.Callback ************************************************************
|
||||
// TimelineEventController.Callback ************************************************************
|
||||
|
||||
override fun onUrlClicked(url: String, title: String): Boolean {
|
||||
permalinkHandler
|
||||
|
@ -1583,7 +1722,14 @@ class RoomDetailFragment @Inject constructor(
|
|||
Snackbar.make(requireView(), message, duration).show()
|
||||
}
|
||||
|
||||
// VectorInviteView.Callback
|
||||
private fun showDialogWithMessage(message: String) {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setMessage(message)
|
||||
.setPositiveButton(getString(R.string.ok), null)
|
||||
.show()
|
||||
}
|
||||
|
||||
// VectorInviteView.Callback
|
||||
|
||||
override fun onAcceptInvite() {
|
||||
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
|
||||
|
@ -1595,7 +1741,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
roomDetailViewModel.handle(RoomDetailAction.RejectInvite)
|
||||
}
|
||||
|
||||
// JumpToReadMarkerView.Callback
|
||||
// JumpToReadMarkerView.Callback
|
||||
|
||||
override fun onJumpToReadMarkerClicked() = withState(roomDetailViewModel) {
|
||||
jumpToReadMarkerView.isVisible = false
|
||||
|
@ -1611,7 +1757,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
roomDetailViewModel.handle(RoomDetailAction.MarkAllAsRead)
|
||||
}
|
||||
|
||||
// AttachmentTypeSelectorView.Callback
|
||||
// AttachmentTypeSelectorView.Callback
|
||||
|
||||
override fun onTypeSelected(type: AttachmentTypeSelectorView.Type) {
|
||||
if (checkPermissions(type.permissionsBit, this, PERMISSION_REQUEST_CODE_PICK_ATTACHMENT)) {
|
||||
|
@ -1632,7 +1778,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
}.exhaustive
|
||||
}
|
||||
|
||||
// AttachmentsHelper.Callback
|
||||
// AttachmentsHelper.Callback
|
||||
|
||||
override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) {
|
||||
if (roomDetailViewModel.preventAttachmentPreview) {
|
||||
|
@ -1662,7 +1808,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
roomDetailViewModel.handle(RoomDetailAction.SendMessage(formattedContact, false))
|
||||
}
|
||||
|
||||
override fun onViewWidgetsClicked() {
|
||||
private fun onViewWidgetsClicked() {
|
||||
RoomWidgetsBottomSheet.newInstance()
|
||||
.show(childFragmentManager, "ROOM_WIDGETS_BOTTOM_SHEET")
|
||||
}
|
||||
|
|
|
@ -35,9 +35,14 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
|
|||
data class ActionFailure(val action: RoomDetailAction, val throwable: Throwable) : RoomDetailViewEvents()
|
||||
|
||||
data class ShowMessage(val message: String) : RoomDetailViewEvents()
|
||||
data class ShowInfoOkDialog(val message: String) : RoomDetailViewEvents()
|
||||
data class ShowE2EErrorMessage(val withHeldCode: WithHeldCode?) : RoomDetailViewEvents()
|
||||
|
||||
data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents()
|
||||
data class JoinJitsiConference(val widget: Widget, val withVideo: Boolean) : RoomDetailViewEvents()
|
||||
|
||||
object ShowWaitingView: RoomDetailViewEvents()
|
||||
object HideWaitingView: RoomDetailViewEvents()
|
||||
|
||||
data class FileTooBigError(
|
||||
val filename: String,
|
||||
|
@ -66,6 +71,10 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
|
|||
data class OpenStickerPicker(val widget: Widget): RoomDetailViewEvents()
|
||||
|
||||
object OpenIntegrationManager: RoomDetailViewEvents()
|
||||
object OpenActiveWidgetBottomSheet: RoomDetailViewEvents()
|
||||
data class RequestNativeWidgetPermission(val widget: Widget,
|
||||
val domain: String,
|
||||
val grantedEvents: RoomDetailViewEvents) : RoomDetailViewEvents()
|
||||
|
||||
object MessageSent : SendMessageResult()
|
||||
data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult()
|
||||
|
|
|
@ -43,7 +43,17 @@ import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
|||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
|
||||
import im.vector.app.features.home.room.typing.TypingHelper
|
||||
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
|
||||
import im.vector.app.features.settings.VectorLocale
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.functions.BiFunction
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.MatrixPatterns
|
||||
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
||||
|
@ -76,23 +86,18 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
|||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent
|
||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
import org.matrix.android.sdk.rx.rx
|
||||
import org.matrix.android.sdk.rx.unwrap
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.functions.BiFunction
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
|
@ -188,8 +193,12 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
PowerLevelsObservableFactory(room).createObservable()
|
||||
.subscribe {
|
||||
val canSendMessage = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE)
|
||||
val isAllowedToManageWidgets = session.widgetService().hasPermissionsToHandleWidgets(room.roomId)
|
||||
setState {
|
||||
copy(canSendMessage = canSendMessage)
|
||||
copy(
|
||||
canSendMessage = canSendMessage,
|
||||
isAllowedToManageWidgets = isAllowedToManageWidgets
|
||||
)
|
||||
}
|
||||
}
|
||||
.disposeOnClear()
|
||||
|
@ -269,6 +278,10 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
|
||||
is RoomDetailAction.StartCall -> handleStartCall(action)
|
||||
is RoomDetailAction.EndCall -> handleEndCall()
|
||||
is RoomDetailAction.ManageIntegrations -> handleManageIntegrations()
|
||||
is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action)
|
||||
is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId)
|
||||
is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action)
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
|
@ -306,6 +319,115 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleManageIntegrations() = withState { state ->
|
||||
if (state.activeRoomWidgets().isNullOrEmpty()) {
|
||||
// Directly open integration manager screen
|
||||
handleOpenIntegrationManager()
|
||||
} else {
|
||||
// Display bottomsheet with widget list
|
||||
_viewEvents.post(RoomDetailViewEvents.OpenActiveWidgetBottomSheet)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAddJitsiConference(action: RoomDetailAction.AddJitsiWidget) {
|
||||
_viewEvents.post(RoomDetailViewEvents.ShowWaitingView)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// Build data for a jitsi widget
|
||||
val widgetId: String = WidgetType.Jitsi.preferred + "_" + session.myUserId + "_" + System.currentTimeMillis()
|
||||
|
||||
// Create a random enough jitsi conference id
|
||||
// Note: the jitsi server automatically creates conference when the conference
|
||||
// id does not exist yet
|
||||
var widgetSessionId = UUID.randomUUID().toString()
|
||||
|
||||
if (widgetSessionId.length > 8) {
|
||||
widgetSessionId = widgetSessionId.substring(0, 7)
|
||||
}
|
||||
val roomId: String = room.roomId
|
||||
val confId = roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.toLowerCase(VectorLocale.applicationLocale)
|
||||
|
||||
val jitsiDomain = session.getHomeServerCapabilities().preferredJitsiDomain ?: stringProvider.getString(R.string.preferred_jitsi_domain)
|
||||
|
||||
// We use the default element wrapper for this widget
|
||||
// https://github.com/vector-im/element-web/blob/develop/docs/jitsi-dev.md
|
||||
val url = "https://app.element.io/jitsi.html" +
|
||||
"?confId=$confId" +
|
||||
"#conferenceDomain=\$domain" +
|
||||
"&conferenceId=\$conferenceId" +
|
||||
"&isAudioOnly=${!action.withVideo}" +
|
||||
"&displayName=\$matrix_display_name" +
|
||||
"&avatarUrl=\$matrix_avatar_url" +
|
||||
"&userId=\$matrix_user_id"
|
||||
|
||||
val widgetEventContent = mapOf(
|
||||
"url" to url,
|
||||
"type" to WidgetType.Jitsi.legacy,
|
||||
"data" to mapOf(
|
||||
"conferenceId" to confId,
|
||||
"domain" to jitsiDomain,
|
||||
"isAudioOnly" to !action.withVideo
|
||||
),
|
||||
"creatorUserId" to session.myUserId,
|
||||
"id" to widgetId,
|
||||
"name" to "jitsi"
|
||||
)
|
||||
|
||||
try {
|
||||
val widget = awaitCallback<Widget> {
|
||||
session.widgetService().createRoomWidget(roomId, widgetId, widgetEventContent, it)
|
||||
}
|
||||
_viewEvents.post(RoomDetailViewEvents.JoinJitsiConference(widget, action.withVideo))
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.failed_to_add_widget)))
|
||||
} finally {
|
||||
_viewEvents.post(RoomDetailViewEvents.HideWaitingView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDeleteWidget(widgetId: String) {
|
||||
_viewEvents.post(RoomDetailViewEvents.ShowWaitingView)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
awaitCallback<Unit> { session.widgetService().destroyRoomWidget(room.roomId, widgetId, it) }
|
||||
// local echo
|
||||
setState {
|
||||
copy(
|
||||
activeRoomWidgets = when (activeRoomWidgets) {
|
||||
is Success -> {
|
||||
Success(activeRoomWidgets.invoke().filter { it.widgetId != widgetId })
|
||||
}
|
||||
else -> activeRoomWidgets
|
||||
}
|
||||
)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.failed_to_remove_widget)))
|
||||
} finally {
|
||||
_viewEvents.post(RoomDetailViewEvents.HideWaitingView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCheckWidgetAllowed(action: RoomDetailAction.EnsureNativeWidgetAllowed) {
|
||||
val widget = action.widget
|
||||
val domain = action.widget.widgetContent.data["domain"] as? String ?: ""
|
||||
val isAllowed = action.userJustAccepted || if (widget.type == WidgetType.Jitsi) {
|
||||
widget.senderInfo?.userId == session.myUserId
|
||||
|| session.integrationManagerService().isNativeWidgetDomainAllowed(
|
||||
action.widget.type.preferred,
|
||||
domain
|
||||
)
|
||||
} else false
|
||||
|
||||
if (isAllowed) {
|
||||
_viewEvents.post(action.grantedEvents)
|
||||
} else {
|
||||
// we need to request permission
|
||||
_viewEvents.post(RoomDetailViewEvents.RequestNativeWidgetPermission(widget, domain, action.grantedEvents))
|
||||
}
|
||||
}
|
||||
|
||||
private fun startTrackingUnreadMessages() {
|
||||
trackUnreadMessages.set(true)
|
||||
setState { copy(canShowJumpToReadMarker = false) }
|
||||
|
@ -419,7 +541,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true
|
||||
R.id.open_matrix_apps -> true
|
||||
R.id.voice_call,
|
||||
R.id.video_call -> state.asyncRoomSummary()?.canStartCall == true && webRtcPeerConnectionManager.currentCall == null
|
||||
R.id.video_call -> true // always show for discoverability
|
||||
R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null
|
||||
else -> false
|
||||
}
|
||||
|
|
|
@ -66,7 +66,8 @@ data class RoomDetailViewState(
|
|||
val unreadState: UnreadState = UnreadState.Unknown,
|
||||
val canShowJumpToReadMarker: Boolean = true,
|
||||
val changeMembershipState: ChangeMembershipState = ChangeMembershipState.Unknown,
|
||||
val canSendMessage: Boolean = true
|
||||
val canSendMessage: Boolean = true,
|
||||
val isAllowedToManageWidgets: Boolean = false
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -16,7 +16,10 @@
|
|||
|
||||
package im.vector.app.features.home.room.detail.widget
|
||||
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import com.airbnb.epoxy.EpoxyModelWithHolder
|
||||
|
@ -24,21 +27,34 @@ import im.vector.app.R
|
|||
import im.vector.app.core.epoxy.ClickListener
|
||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import org.matrix.android.sdk.api.extensions.tryThis
|
||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||
import java.net.URL
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_room_widget)
|
||||
abstract class RoomWidgetItem : EpoxyModelWithHolder<RoomWidgetItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute lateinit var widget: Widget
|
||||
@EpoxyAttribute var widgetClicked: ClickListener? = null
|
||||
@DrawableRes
|
||||
@EpoxyAttribute var iconRes: Int? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
holder.widgetName.text = widget.name
|
||||
holder.widgetUrl.text = tryThis { URL(widget.computedUrl) }?.host ?: widget.computedUrl
|
||||
if (iconRes != null) {
|
||||
holder.iconImage.isVisible = true
|
||||
holder.iconImage.setImageResource(iconRes!!)
|
||||
} else {
|
||||
holder.iconImage.isVisible = false
|
||||
}
|
||||
holder.view.onClick(widgetClicked)
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val widgetName by bind<TextView>(R.id.roomWidgetName)
|
||||
val widgetUrl by bind<TextView>(R.id.roomWidgetUrl)
|
||||
val iconImage by bind<ImageView>(R.id.roomWidgetAvatar)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import im.vector.app.core.extensions.cleanup
|
|||
import im.vector.app.core.extensions.configureWith
|
||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.features.home.room.detail.RoomDetailAction
|
||||
import im.vector.app.features.home.room.detail.RoomDetailViewModel
|
||||
import im.vector.app.features.home.room.detail.RoomDetailViewState
|
||||
import im.vector.app.features.navigation.Navigator
|
||||
|
@ -37,9 +38,9 @@ import javax.inject.Inject
|
|||
/**
|
||||
* Bottom sheet displaying active widgets in a room
|
||||
*/
|
||||
class RoomWidgetsBottomSheet : VectorBaseBottomSheetDialogFragment(), RoomWidgetController.Listener {
|
||||
class RoomWidgetsBottomSheet : VectorBaseBottomSheetDialogFragment(), RoomWidgetsController.Listener {
|
||||
|
||||
@Inject lateinit var epoxyController: RoomWidgetController
|
||||
@Inject lateinit var epoxyController: RoomWidgetsController
|
||||
@Inject lateinit var colorProvider: ColorProvider
|
||||
@Inject lateinit var navigator: Navigator
|
||||
|
||||
|
@ -77,6 +78,11 @@ class RoomWidgetsBottomSheet : VectorBaseBottomSheetDialogFragment(), RoomWidget
|
|||
dismiss()
|
||||
}
|
||||
|
||||
override fun didSelectManageWidgets() {
|
||||
roomDetailViewModel.handle(RoomDetailAction.OpenIntegrationManager)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(): RoomWidgetsBottomSheet {
|
||||
return RoomWidgetsBottomSheet()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -31,6 +31,8 @@ import im.vector.app.core.di.ActiveSessionHolder
|
|||
import im.vector.app.core.error.fatalError
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.core.utils.toast
|
||||
import im.vector.app.features.call.conference.JitsiCallViewModel
|
||||
import im.vector.app.features.call.conference.VectorJitsiActivity
|
||||
import im.vector.app.features.createdirect.CreateDirectRoomActivity
|
||||
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||
import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity
|
||||
|
@ -66,6 +68,7 @@ import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom
|
|||
import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData
|
||||
import org.matrix.android.sdk.api.session.terms.TermsService
|
||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
@ -270,9 +273,14 @@ class DefaultNavigator @Inject constructor(
|
|||
fragment.startActivityForResult(intent, WidgetRequestCodes.INTEGRATION_MANAGER_REQUEST_CODE)
|
||||
}
|
||||
|
||||
override fun openRoomWidget(context: Context, roomId: String, widget: Widget) {
|
||||
val widgetArgs = widgetArgsBuilder.buildRoomWidgetArgs(roomId, widget)
|
||||
context.startActivity(WidgetActivity.newIntent(context, widgetArgs))
|
||||
override fun openRoomWidget(context: Context, roomId: String, widget: Widget, options: Map<String, Any>?) {
|
||||
if (widget.type is WidgetType.Jitsi) {
|
||||
val enableVideo = options?.get(JitsiCallViewModel.ENABLE_VIDEO_OPTION) == true
|
||||
context.startActivity(VectorJitsiActivity.newIntent(context, roomId = roomId, widgetId = widget.widgetId, enableVideo = enableVideo))
|
||||
} else {
|
||||
val widgetArgs = widgetArgsBuilder.buildRoomWidgetArgs(roomId, widget)
|
||||
context.startActivity(WidgetActivity.newIntent(context, widgetArgs))
|
||||
}
|
||||
}
|
||||
|
||||
override fun openPinCode(fragment: Fragment, pinMode: PinMode, requestCode: Int) {
|
||||
|
|
|
@ -97,7 +97,7 @@ interface Navigator {
|
|||
|
||||
fun openIntegrationManager(fragment: Fragment, roomId: String, integId: String?, screen: String?)
|
||||
|
||||
fun openRoomWidget(context: Context, roomId: String, widget: Widget)
|
||||
fun openRoomWidget(context: Context, roomId: String, widget: Widget, options: Map<String, Any>? = null)
|
||||
|
||||
fun openMediaViewer(activity: Activity,
|
||||
roomId: String,
|
||||
|
|
|
@ -111,7 +111,7 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
|
|||
// id("complete_security")
|
||||
// iconRes(R.drawable.ic_shield_warning)
|
||||
// text(stringProvider.getString(R.string.complete_security))
|
||||
// itemClickAction(DebouncedClickListener(View.OnClickListener { _ ->
|
||||
// buttonClickAction(DebouncedClickListener(View.OnClickListener { _ ->
|
||||
// callback?.completeSecurity()
|
||||
// }))
|
||||
// }
|
||||
|
|
|
@ -48,6 +48,9 @@ class RoomWidgetPermissionBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
|||
injector.inject(this)
|
||||
}
|
||||
|
||||
// Use this if you don't need the full activity view model
|
||||
var directListener: ((Boolean) -> Unit)? = null
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
super.invalidate()
|
||||
val permissionData = state.permissionData() ?: return@withState
|
||||
|
@ -88,6 +91,7 @@ class RoomWidgetPermissionBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
|||
@OnClick(R.id.widgetPermissionDecline)
|
||||
fun doDecline() {
|
||||
viewModel.handle(RoomWidgetPermissionActions.BlockWidget)
|
||||
directListener?.invoke(false)
|
||||
// optimistic dismiss
|
||||
dismiss()
|
||||
}
|
||||
|
@ -95,6 +99,7 @@ class RoomWidgetPermissionBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
|||
@OnClick(R.id.widgetPermissionContinue)
|
||||
fun doAccept() {
|
||||
viewModel.handle(RoomWidgetPermissionActions.AllowWidget)
|
||||
directListener?.invoke(true)
|
||||
// optimistic dismiss
|
||||
dismiss()
|
||||
}
|
||||
|
|
|
@ -28,6 +28,8 @@ import org.matrix.android.sdk.api.session.Session
|
|||
import im.vector.app.R
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
import org.matrix.android.sdk.rx.rx
|
||||
import timber.log.Timber
|
||||
import java.net.URL
|
||||
|
@ -57,20 +59,33 @@ class RoomWidgetPermissionViewModel @AssistedInject constructor(@Assisted val in
|
|||
}
|
||||
// TODO check from widget urls the perms that should be shown?
|
||||
// For now put all
|
||||
val infoShared = listOf(
|
||||
R.string.room_widget_permission_display_name,
|
||||
R.string.room_widget_permission_avatar_url,
|
||||
R.string.room_widget_permission_user_id,
|
||||
R.string.room_widget_permission_theme,
|
||||
R.string.room_widget_permission_widget_id,
|
||||
R.string.room_widget_permission_room_id
|
||||
)
|
||||
RoomWidgetPermissionViewState.WidgetPermissionData(
|
||||
widget = widget,
|
||||
isWebviewWidget = true,
|
||||
permissionsList = infoShared,
|
||||
widgetDomain = domain
|
||||
)
|
||||
if (widget.type == WidgetType.Jitsi) {
|
||||
val infoShared = listOf(
|
||||
R.string.room_widget_permission_display_name,
|
||||
R.string.room_widget_permission_avatar_url
|
||||
)
|
||||
RoomWidgetPermissionViewState.WidgetPermissionData(
|
||||
widget = widget,
|
||||
isWebviewWidget = false,
|
||||
permissionsList = infoShared,
|
||||
widgetDomain = widget.widgetContent.data["domain"] as? String
|
||||
)
|
||||
} else {
|
||||
val infoShared = listOf(
|
||||
R.string.room_widget_permission_display_name,
|
||||
R.string.room_widget_permission_avatar_url,
|
||||
R.string.room_widget_permission_user_id,
|
||||
R.string.room_widget_permission_theme,
|
||||
R.string.room_widget_permission_widget_id,
|
||||
R.string.room_widget_permission_room_id
|
||||
)
|
||||
RoomWidgetPermissionViewState.WidgetPermissionData(
|
||||
widget = widget,
|
||||
isWebviewWidget = true,
|
||||
permissionsList = infoShared,
|
||||
widgetDomain = domain
|
||||
)
|
||||
}
|
||||
}
|
||||
.execute {
|
||||
copy(permissionData = it)
|
||||
|
@ -91,7 +106,14 @@ class RoomWidgetPermissionViewModel @AssistedInject constructor(@Assisted val in
|
|||
if (state.permissionData()?.isWebviewWidget.orFalse()) {
|
||||
WidgetPermissionsHelper(integrationManagerService, widgetService).changePermission(state.roomId, widgetId, false)
|
||||
} else {
|
||||
// TODO JITSI
|
||||
awaitCallback<Unit> {
|
||||
session.integrationManagerService().setNativeWidgetDomainAllowed(
|
||||
state.permissionData.invoke()?.widget?.type?.preferred ?: "",
|
||||
state.permissionData.invoke()?.widgetDomain ?: "",
|
||||
false,
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.v("Failure revoking widget: ${state.widgetId}")
|
||||
|
@ -109,7 +131,14 @@ class RoomWidgetPermissionViewModel @AssistedInject constructor(@Assisted val in
|
|||
if (state.permissionData()?.isWebviewWidget.orFalse()) {
|
||||
WidgetPermissionsHelper(integrationManagerService, widgetService).changePermission(state.roomId, widgetId, true)
|
||||
} else {
|
||||
// TODO JITSI
|
||||
awaitCallback<Unit> {
|
||||
session.integrationManagerService().setNativeWidgetDomainAllowed(
|
||||
state.permissionData.invoke()?.widget?.type?.preferred ?: "",
|
||||
state.permissionData.invoke()?.widgetDomain ?: "",
|
||||
true,
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.v("Failure allowing widget: ${state.widgetId}")
|
||||
|
|
10
vector/src/main/res/drawable/ic_integrations.xml
Normal file
10
vector/src/main/res/drawable/ic_integrations.xml
Normal 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>
|
25
vector/src/main/res/layout/activity_jitsi.xml
Normal file
25
vector/src/main/res/layout/activity_jitsi.xml
Normal 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>
|
|
@ -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>
|
|
@ -104,6 +104,14 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/syncStateView"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<im.vector.app.core.ui.views.ActiveConferenceView
|
||||
android:id="@+id/activeConferenceView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/activeCallView"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="0dp"
|
||||
|
@ -112,7 +120,7 @@
|
|||
app:layout_constraintBottom_toTopOf="@+id/recyclerViewBarrier"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/activeCallView"
|
||||
app:layout_constraintTop_toBottomOf="@id/activeConferenceView"
|
||||
tools:listitem="@layout/item_timeline_event_base" />
|
||||
|
||||
<FrameLayout
|
||||
|
@ -121,17 +129,17 @@
|
|||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/activeCallView">
|
||||
app:layout_constraintTop_toBottomOf="@id/activeConferenceView">
|
||||
|
||||
<im.vector.app.features.home.room.detail.widget.RoomWidgetsBannerView
|
||||
android:id="@+id/roomWidgetsBannerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
<!-- <im.vector.app.features.home.room.detail.widget.RoomWidgetsBannerView-->
|
||||
<!-- android:id="@+id/roomWidgetsBannerView"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="wrap_content"-->
|
||||
<!-- android:layout_marginStart="8dp"-->
|
||||
<!-- android:layout_marginTop="8dp"-->
|
||||
<!-- android:layout_marginEnd="8dp"-->
|
||||
<!-- android:visibility="gone"-->
|
||||
<!-- tools:visibility="visible" />-->
|
||||
|
||||
<im.vector.app.core.ui.views.JumpToReadMarkerView
|
||||
android:id="@+id/jumpToReadMarkerView"
|
||||
|
@ -190,7 +198,7 @@
|
|||
android:focusable="true"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/activeCallView">
|
||||
app:layout_constraintTop_toBottomOf="@id/activeConferenceView">
|
||||
|
||||
<org.webrtc.SurfaceViewRenderer
|
||||
android:id="@+id/activeCallPiP"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -16,11 +16,32 @@
|
|||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/roomWidgetName"
|
||||
style="@style/BottomSheetItemTextMain"
|
||||
tools:text="@sample/matrix.json/data/displayName" />
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/roomWidgetUrl"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/roomWidgetAvatar"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="Widget name" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/roomWidgetUrl"
|
||||
style="@style/BottomSheetItemTextSecondary"
|
||||
android:textStyle="normal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@id/roomWidgetName"
|
||||
app:layout_constraintStart_toStartOf="@id/roomWidgetName"
|
||||
app:layout_constraintTop_toBottomOf="@id/roomWidgetName"
|
||||
tools:text="https://foobar" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -25,8 +25,9 @@
|
|||
android:textColor="@color/white"
|
||||
app:drawableTint="@color/white" />
|
||||
|
||||
<TextView
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/returnToCallButton"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignTop="@+id/activeCallInfo"
|
||||
|
@ -38,7 +39,6 @@
|
|||
android:paddingStart="8dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:text="@string/return_to_call"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold" />
|
||||
|
|
45
vector/src/main/res/layout/view_active_conference_view.xml
Normal file
45
vector/src/main/res/layout/view_active_conference_view.xml
Normal 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>
|
|
@ -12,11 +12,6 @@
|
|||
app:showAsAction="always"
|
||||
tools:visible="true" />
|
||||
|
||||
<item
|
||||
android:id="@+id/open_matrix_apps"
|
||||
android:title="@string/room_add_matrix_apps"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/voice_call"
|
||||
android:icon="@drawable/ic_phone"
|
||||
|
@ -35,6 +30,12 @@
|
|||
app:showAsAction="always"
|
||||
tools:visible="true" />
|
||||
|
||||
<item
|
||||
android:id="@+id/open_matrix_apps"
|
||||
android:title="@string/room_add_matrix_apps"
|
||||
app:actionLayout="@layout/custom_action_item_layout_badge"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/resend_all"
|
||||
android:icon="@drawable/ic_refresh_cw"
|
||||
|
|
|
@ -18,6 +18,9 @@
|
|||
<!-- Note: pusher_app_id cannot exceed 64 chars -->
|
||||
<string name="pusher_app_id" translatable="false">im.vector.app.android</string>
|
||||
|
||||
<!-- preferred jitsi domain -->
|
||||
<string name="preferred_jitsi_domain" translatable="false">jitsi.riot.im</string>
|
||||
|
||||
<string-array name="room_directory_servers" translatable="false">
|
||||
<item>matrix.org</item>
|
||||
</string-array>
|
||||
|
|
|
@ -89,8 +89,17 @@
|
|||
<string name="missing_permissions_warning">"Due to missing permissions, some features may be missing…</string>
|
||||
<string name="missing_permissions_error">"Due to missing permissions, this action is not possible.</string>
|
||||
<string name="missing_permissions_to_start_conf_call">You need permission to invite to start a conference in this room</string>
|
||||
<string name="no_permissions_to_start_conf_call">You do not have permission to start a conference call in this room</string>
|
||||
<string name="conference_call_in_progress">A conference is already in progress!</string>
|
||||
<string name="video_meeting">Start video meeting</string>
|
||||
<string name="audio_meeting">Start audio meeting</string>
|
||||
<string name="audio_video_meeting_description">Meetings use Jitsi security and permission policies. All people currently in the room will see an invite to join while your meeting is happening.</string>
|
||||
<string name="missing_permissions_title_to_start_conf_call">Cannot start call</string>
|
||||
<string name="cannot_call_yourself">You cannot place a call with yourself</string>
|
||||
<string name="cannot_call_yourself_with_invite">You cannot place a call with yourself, wait for participants to accept invitation</string>
|
||||
<string name="device_information">Session information</string>
|
||||
<string name="failed_to_add_widget">Failed to add widget</string>
|
||||
<string name="failed_to_remove_widget">Failed to remove widget</string>
|
||||
<string name="room_no_conference_call_in_encrypted_rooms">Conference calls are not supported in encrypted rooms</string>
|
||||
<string name="call_anyway">Call Anyway</string>
|
||||
<string name="send_anyway">Send Anyway</string>
|
||||
|
@ -1210,6 +1219,8 @@
|
|||
<string name="widget_integration_invalid_parameter">A parameter is not valid.</string>
|
||||
<string name="integration_manager_not_configured">No integration manager configured.</string>
|
||||
<string name="room_add_matrix_apps">Add Matrix apps</string>
|
||||
<string name="room_manage_integrations">Manage Integrations</string>
|
||||
<string name="room_no_active_widgets">No active widgets</string>
|
||||
<string name="settings_labs_native_camera">Use native camera</string>
|
||||
<string name="settings_labs_native_camera_summary">Start the system camera instead of the custom camera screen.</string>
|
||||
<string name="settings_labs_keyboard_options_to_send_message">Use keyboard enter key to send message</string>
|
||||
|
|
|
@ -331,6 +331,17 @@
|
|||
<item name="android:textSize">16sp</item>
|
||||
</style>
|
||||
|
||||
<style name="BottomSheetItemTextSecondary">
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
<item name="android:layout_width">0dp</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:layout_weight">1</item>
|
||||
<item name="android:ellipsize">end</item>
|
||||
<item name="android:maxLines">2</item>
|
||||
<item name="android:textColor">?riotx_text_secondary</item>
|
||||
<item name="android:textSize">14sp</item>
|
||||
</style>
|
||||
|
||||
<style name="BottomSheetItemTime">
|
||||
<item name="android:layout_width">wrap_content</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
|
|
Loading…
Add table
Reference in a new issue