From 42a24300a129036adba5e2ad8b2ca0f402c8b60b Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 12 Aug 2020 14:02:00 +0200 Subject: [PATCH] Initial commit jitsi --- .idea/dictionaries/bmarty.xml | 1 + build.gradle | 4 + matrix-sdk-android/build.gradle | 13 +- .../android/sdk/internal/di/NetworkModule.kt | 6 +- vector/build.gradle | 5 +- vector/proguard-rules.pro | 41 +++++ vector/src/main/AndroidManifest.xml | 3 + .../im/vector/app/core/di/ScreenComponent.kt | 2 + .../app/core/ui/list/GenericButtonItem.kt | 2 +- .../app/core/ui/views/ActiveConferenceView.kt | 81 +++++++++ .../call/conference/JitsiCallViewModel.kt | 118 +++++++++++++ .../call/conference/JitsiCallViewState.kt | 34 ++++ .../call/conference/JitsiWidgetProperties.kt | 37 ++++ .../call/conference/VectorJitsiActivity.kt | 167 ++++++++++++++++++ .../home/room/detail/RoomDetailAction.kt | 1 + .../home/room/detail/RoomDetailFragment.kt | 72 +++++++- .../home/room/detail/RoomDetailViewEvents.kt | 1 + .../home/room/detail/RoomDetailViewModel.kt | 11 ++ .../detail/widget/RoomWidgetController.kt | 36 +++- .../home/room/detail/widget/RoomWidgetItem.kt | 16 ++ .../detail/widget/RoomWidgetsBottomSheet.kt | 6 + .../features/navigation/DefaultNavigator.kt | 14 +- .../app/features/navigation/Navigator.kt | 2 +- .../src/main/res/drawable/ic_integrations.xml | 10 ++ vector/src/main/res/layout/activity_jitsi.xml | 32 ++++ .../custom_action_item_layout_badge.xml | 31 ++++ .../main/res/layout/fragment_room_detail.xml | 28 +-- .../src/main/res/layout/item_room_widget.xml | 27 ++- .../layout/view_active_conference_view.xml | 47 +++++ vector/src/main/res/menu/menu_timeline.xml | 11 +- vector/src/main/res/values/strings.xml | 2 + vector/src/main/res/values/styles_riot.xml | 11 ++ 32 files changed, 829 insertions(+), 43 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/ui/views/ActiveConferenceView.kt create mode 100644 vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewState.kt create mode 100644 vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetProperties.kt create mode 100644 vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt create mode 100644 vector/src/main/res/drawable/ic_integrations.xml create mode 100644 vector/src/main/res/layout/activity_jitsi.xml create mode 100644 vector/src/main/res/layout/custom_action_item_layout_badge.xml create mode 100644 vector/src/main/res/layout/view_active_conference_view.xml diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index 9d601bff14..5c27da044e 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -14,6 +14,7 @@ gplay hmac homeserver + jitsi ktlint linkified linkify diff --git a/build.gradle b/build.gradle index 12c5181ea4..3a9bc2a991 100644 --- a/build.gradle +++ b/build.gradle @@ -52,6 +52,10 @@ allprojects { } } maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } + // Jitsi repo + maven { + url "https://github.com/vector-im/jitsi_libre_maven/raw/master/releases" + } google() jcenter() } diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index a96e0690dc..57a9dfaa87 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -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" @@ -175,7 +180,9 @@ dependencies { // 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.+' + // implementation 'org.webrtc:google-webrtc:1.0.+' + // WebRTC + implementation('com.facebook.react:react-native-webrtc:1.69.2-jitsi-2062090@aar') debugImplementation 'com.airbnb.okreplay:okreplay:1.5.0' releaseImplementation 'com.airbnb.okreplay:noop:1.5.0' diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt index 71961d02d3..5fff658a56 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt @@ -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) diff --git a/vector/build.gradle b/vector/build.gradle index a1b0c006cf..c5d95f8d38 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -406,7 +406,10 @@ dependencies { implementation 'com.github.BillCarsonFr:JsonViewer:0.5' // TODO meant for development purposes only - implementation 'org.webrtc:google-webrtc:1.0.+' +// implementation 'org.webrtc:google-webrtc:1.0.+' + // WebRTC +// implementation('com.facebook.react:react-native-webrtc:1.69.2-jitsi-2062090@aar') + implementation('org.jitsi.react:jitsi-meet-sdk:2.2.2') { transitive = true } // QR-code // Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170 diff --git a/vector/proguard-rules.pro b/vector/proguard-rules.pro index 7fcfce61b8..46fae8338c 100644 --- a/vector/proguard-rules.pro +++ b/vector/proguard-rules.pro @@ -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 ; } +-keepclassmembers class * { @com.facebook.react.uimanager.UIProp ; } +-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp ; } +-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup ; } + +-dontwarn com.facebook.react.** +-keep,includedescriptorclasses class com.facebook.react.bridge.** { *; } \ No newline at end of file diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 16fc952419..95baf304d0 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -202,6 +202,9 @@ android:name="im.vector.app.features.attachments.preview.AttachmentsPreviewActivity" android:theme="@style/AppTheme.AttachmentsPreview" /> + diff --git a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt index 265c0ec4fc..d337ec7977 100644 --- a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericButtonItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericButtonItem.kt index 69f0a693cb..d080cf90ed 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericButtonItem.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericButtonItem.kt @@ -57,7 +57,7 @@ abstract class GenericButtonItem : VectorEpoxyModel() holder.button.icon = null } - itemClickAction?.let { holder.view.setOnClickListener(it) } + itemClickAction?.let { holder.button.setOnClickListener(it) } } class Holder : VectorEpoxyHolder() { diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ActiveConferenceView.kt b/vector/src/main/java/im/vector/app/core/ui/views/ActiveConferenceView.kt new file mode 100644 index 0000000000..d9870fac8b --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/views/ActiveConferenceView.kt @@ -0,0 +1,81 @@ +/* + * 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 im.vector.app.R +import im.vector.app.core.utils.tappableMatchingText +import im.vector.app.features.themes.ThemeUtils +import org.matrix.android.sdk.api.session.widgets.model.Widget + +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) + } + + 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(R.id.activeConferenceInfo).apply { + text = styledText + movementMethod = LinkMovementMethod.getInstance() + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt new file mode 100644 index 0000000000..e5f031fbef --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt @@ -0,0 +1,118 @@ +/* + * 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.VectorViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.features.call.WebRtcPeerConnectionManager +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 + +sealed class JitsiCallViewActions : VectorViewModelAction + +sealed class JitsiCallViewEvents : VectorViewEvents + +class JitsiCallViewModel @AssistedInject constructor( + @Assisted initialState: JitsiCallViewState, + @Assisted val args: VectorJitsiActivity.Args, + val session: Session, + val webRtcPeerConnectionManager: WebRtcPeerConnectionManager +) : VectorViewModel(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?.displayName + 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 { JitsiWidgetProperties(it) } + 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 { + + 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() +// val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() + + return JitsiCallViewState( + roomId = args.roomId, + widgetId = args.widgetId, + enableVideo = args.enableVideo + ) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewState.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewState.kt new file mode 100644 index 0000000000..72b87e7f1b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewState.kt @@ -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 = Uninitialized +) : MvRxState diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetProperties.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetProperties.kt new file mode 100644 index 0000000000..c8d8f1f27f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetProperties.kt @@ -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 + +class JitsiWidgetProperties(private val uriString: String) { + val domain: String by lazy { configs["conferenceDomain"] ?: DEFAULT_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 by lazy { + configString?.split("&") + ?.map { it.split("=") } + ?.map { (key, value) -> key to value } + ?.toMap() + ?: mapOf() + } +} + +private const val DEFAULT_JITSI_DOMAIN = "jitsi.riot.im" diff --git a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt new file mode 100644 index 0000000000..eda284bce6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt @@ -0,0 +1,167 @@ +/* + * 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.widget.FrameLayout +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 + + 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 -> { +// val widget = viewState.widget.invoke() + configureJitsiView(viewState) + } + } + } + + 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?, requestCode: Int, listener: PermissionListener?) { + JitsiMeetActivityDelegate.requestPermissions(this, permissions, requestCode, listener) + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + JitsiMeetActivityDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + + override fun onConferenceTerminated(p0: MutableMap?) { + finish() + } + + override fun onConferenceJoined(p0: MutableMap?) { + } + + override fun onConferenceWillJoin(p0: MutableMap?) { + } + + 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) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 0134fc310f..f815407816 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -80,4 +80,5 @@ sealed class RoomDetailAction : VectorViewModelAction { object SelectStickerAttachment : RoomDetailAction() object OpenIntegrationManager: RoomDetailAction() + object ManageIntegrations: RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 424731fdb0..c4514f6aaf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -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 @@ -183,6 +187,7 @@ 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.session.widgets.model.Widget import timber.log.Timber import java.io.File import java.net.URL @@ -217,7 +222,7 @@ class RoomDetailFragment @Inject constructor( JumpToReadMarkerView.Callback, AttachmentTypeSelectorView.Callback, AttachmentsHelper.Callback, - RoomWidgetsBannerView.Callback, +// RoomWidgetsBannerView.Callback, ActiveCallView.Callback { companion object { @@ -292,7 +297,7 @@ class RoomDetailFragment @Inject constructor( setupJumpToReadMarkerView() setupActiveCallView() setupJumpToBottomView() - setupWidgetsBannerView() + setupConfBannerView() roomToolbarContentView.debouncedClicks { navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId) @@ -350,6 +355,7 @@ class RoomDetailFragment @Inject constructor( is RoomDetailViewEvents.DisplayEnableIntegrationsWarning -> displayDisabledIntegrationDialog() is RoomDetailViewEvents.OpenIntegrationManager -> openIntegrationManager() is RoomDetailViewEvents.OpenFile -> startOpenFileIntent(it) + RoomDetailViewEvents.OpenActiveWidgetBottomSheet -> onViewWidgetsClicked() }.exhaustive } } @@ -363,8 +369,16 @@ class RoomDetailFragment @Inject constructor( ) } - private fun setupWidgetsBannerView() { - roomWidgetsBannerView.callback = this + private fun setupConfBannerView() { + activeConferenceView.callback = object : ActiveConferenceView.Callback { + override fun onTapJoinAudio(jitsiWidget: Widget) { + navigator.openRoomWidget(requireContext(), roomDetailArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to false)) + } + + override fun onTapJoinVideo(jitsiWidget: Widget) { + navigator.openRoomWidget(requireContext(), roomDetailArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to true)) + } + } } private fun openStickerPicker(event: RoomDetailViewEvents.OpenStickerPicker) { @@ -529,10 +543,40 @@ class RoomDetailFragment @Inject constructor( } } + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + 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 findItem = menu.findItem(R.id.open_matrix_apps) + val widgetsCount = state.activeRoomWidgets.invoke()?.size + if (widgetsCount ?: 0 > 0) { + val actionView = findItem.actionView + actionView + .findViewById(R.id.action_view_icon_image) + .setColorFilter(ContextCompat.getColor(requireContext(), R.color.riotx_accent)) + actionView.findViewById(R.id.cart_badge).isVisible = true + actionView.findViewById(R.id.cart_badge).text = "$widgetsCount" + findItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) + } else { + // icon should be default color no badge + val actionView = findItem.actionView + actionView + .findViewById(R.id.action_view_icon_image) + .setColorFilter(ThemeUtils.getColor(requireContext(), R.attr.riotx_text_secondary)) + actionView.findViewById(R.id.cart_badge).isVisible = false + findItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER) + } + } } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -549,7 +593,7 @@ class RoomDetailFragment @Inject constructor( true } R.id.open_matrix_apps -> { - roomDetailViewModel.handle(RoomDetailAction.OpenIntegrationManager) + roomDetailViewModel.handle(RoomDetailAction.ManageIntegrations) true } R.id.voice_call, @@ -873,7 +917,19 @@ class RoomDetailFragment @Inject constructor( renderToolbar(summary, state.typingMessage) val inviter = state.asyncInviter() if (summary?.membership == Membership.JOIN) { - roomWidgetsBannerView.render(state.activeRoomWidgets()) + // We only display banner for 'live' widgets + val activeConf = // for now only jitsi? + state.activeRoomWidgets()?.firstOrNull { + // for now only jitsi? + it.type == WidgetType.Jitsi + } + + if (activeConf == null) { + activeConferenceView.isVisible = false + } else { + activeConferenceView.isVisible = true + activeConferenceView.jitsiWidget = activeConf + } jumpToBottomView.count = summary.notificationCount jumpToBottomView.drawBadge = summary.hasUnreadMessages scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline @@ -1662,7 +1718,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") } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index 0772da498e..89b42f4fc9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -66,6 +66,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents { data class OpenStickerPicker(val widget: Widget): RoomDetailViewEvents() object OpenIntegrationManager: RoomDetailViewEvents() + object OpenActiveWidgetBottomSheet: RoomDetailViewEvents() object MessageSent : SendMessageResult() data class JoinRoomCommandSuccess(val roomId: String) : SendMessageResult() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 5d1111d67d..de63fb04e1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -269,6 +269,7 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() is RoomDetailAction.StartCall -> handleStartCall(action) is RoomDetailAction.EndCall -> handleEndCall() + is RoomDetailAction.ManageIntegrations -> handleManageIntegrations() }.exhaustive } @@ -306,6 +307,16 @@ 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 startTrackingUnreadMessages() { trackUnreadMessages.set(true) setState { copy(canShowJumpToReadMarker = false) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetController.kt index c15dad77ad..77a179e2db 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetController.kt @@ -16,28 +16,48 @@ 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 RoomWidgetController @Inject constructor() : TypedEpoxyController>() { +class RoomWidgetController @Inject constructor(val stringProvider: StringProvider, val colorProvider: ColorProvider) : TypedEpoxyController>() { var listener: Listener? = null - override fun buildModels(widget: List) { - widget.forEach { - RoomWidgetItem_() - .id(it.widgetId) - .widget(it) - .widgetClicked { listener?.didSelectWidget(it) } - .addTo(this) + override fun buildModels(widgets: List) { + 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) } + .addTo(this) + } + } + genericButtonItem { + id("addIntegration") + text(stringProvider.getString(R.string.room_manage_integrations)) + textColor(colorProvider.getColor(R.color.riotx_accent)) + itemClickAction(View.OnClickListener { listener?.didSelectManageWidgets() }) } } interface Listener { fun didSelectWidget(widget: Widget) + fun didSelectManageWidgets() } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetItem.kt index bc6c935102..a58b8291c7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetItem.kt @@ -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() { @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(R.id.roomWidgetName) + val widgetUrl by bind(R.id.roomWidgetUrl) + val iconImage by bind(R.id.roomWidgetAvatar) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsBottomSheet.kt index 4a89c75b7f..32446bfcde 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetsBottomSheet.kt @@ -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 @@ -77,6 +78,11 @@ class RoomWidgetsBottomSheet : VectorBaseBottomSheetDialogFragment(), RoomWidget dismiss() } + override fun didSelectManageWidgets() { + roomDetailViewModel.handle(RoomDetailAction.OpenIntegrationManager) + dismiss() + } + companion object { fun newInstance(): RoomWidgetsBottomSheet { return RoomWidgetsBottomSheet() diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 7ec5c94e4b..3da6a7257a 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -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?) { + 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) { diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 28bb3e3c60..ee64c5fc75 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -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? = null) fun openMediaViewer(activity: Activity, roomId: String, diff --git a/vector/src/main/res/drawable/ic_integrations.xml b/vector/src/main/res/drawable/ic_integrations.xml new file mode 100644 index 0000000000..938b6f7d79 --- /dev/null +++ b/vector/src/main/res/drawable/ic_integrations.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/layout/activity_jitsi.xml b/vector/src/main/res/layout/activity_jitsi.xml new file mode 100644 index 0000000000..8928d298b3 --- /dev/null +++ b/vector/src/main/res/layout/activity_jitsi.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/custom_action_item_layout_badge.xml b/vector/src/main/res/layout/custom_action_item_layout_badge.xml new file mode 100644 index 0000000000..cfa2180219 --- /dev/null +++ b/vector/src/main/res/layout/custom_action_item_layout_badge.xml @@ -0,0 +1,31 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index e5246c43b7..d42020ad69 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -104,6 +104,14 @@ app:layout_constraintTop_toBottomOf="@id/syncStateView" tools:visibility="visible" /> + + - + + + + + + + + + - - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/view_active_conference_view.xml b/vector/src/main/res/layout/view_active_conference_view.xml new file mode 100644 index 0000000000..4195c227a6 --- /dev/null +++ b/vector/src/main/res/layout/view_active_conference_view.xml @@ -0,0 +1,47 @@ + + + + + + + + diff --git a/vector/src/main/res/menu/menu_timeline.xml b/vector/src/main/res/menu/menu_timeline.xml index d4eb923d50..6cdbbed424 100644 --- a/vector/src/main/res/menu/menu_timeline.xml +++ b/vector/src/main/res/menu/menu_timeline.xml @@ -12,11 +12,6 @@ app:showAsAction="always" tools:visible="true" /> - - + + A parameter is not valid. No integration manager configured. Add Matrix apps + Manage Integrations + No active widgets Use native camera Start the system camera instead of the custom camera screen. Use keyboard enter key to send message diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml index b3430a59c8..fee95ab5e2 100644 --- a/vector/src/main/res/values/styles_riot.xml +++ b/vector/src/main/res/values/styles_riot.xml @@ -331,6 +331,17 @@ 16sp + +