Initial commit jitsi

This commit is contained in:
Valere 2020-08-12 14:02:00 +02:00
parent 5a3894036c
commit 42a24300a1
32 changed files with 829 additions and 43 deletions

View file

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

View file

@ -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()
}

View file

@ -132,8 +132,13 @@ dependencies {
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version"
implementation 'com.squareup.okhttp3:okhttp:4.2.2'
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.8.1"))
implementation 'com.squareup.okhttp3:okhttp'
implementation 'com.squareup.okhttp3:logging-interceptor'
implementation("com.squareup.okhttp3:okhttp-urlconnection")
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
@ -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'

View file

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

View file

@ -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

View file

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

View file

@ -202,6 +202,9 @@
android:name="im.vector.app.features.attachments.preview.AttachmentsPreviewActivity"
android:theme="@style/AppTheme.AttachmentsPreview" />
<activity android:name="im.vector.app.features.call.VectorCallActivity" />
<activity
android:name="im.vector.app.features.call.conference.VectorJitsiActivity"
android:configChanges="orientation|screenSize" />
<activity android:name="im.vector.app.features.terms.ReviewTermsActivity" />
<activity android:name="im.vector.app.features.widgets.WidgetActivity" />

View file

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

View file

@ -57,7 +57,7 @@ abstract class GenericButtonItem : VectorEpoxyModel<GenericButtonItem.Holder>()
holder.button.icon = null
}
itemClickAction?.let { holder.view.setOnClickListener(it) }
itemClickAction?.let { holder.button.setOnClickListener(it) }
}
class Holder : VectorEpoxyHolder() {

View file

@ -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<TextView>(R.id.activeConferenceInfo).apply {
text = styledText
movementMethod = LinkMovementMethod.getInstance()
}
}
}

View file

@ -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<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?.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<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()
// val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
return JitsiCallViewState(
roomId = args.roomId,
widgetId = args.widgetId,
enableVideo = args.enableVideo
)
}
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.call.conference
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import org.jitsi.meet.sdk.JitsiMeetUserInfo
import org.matrix.android.sdk.api.session.widgets.model.Widget
data class JitsiCallViewState(
val roomId: String = "",
val widgetId: String = "",
val enableVideo: Boolean = true,
val jitsiUrl: String = "",
val subject: String = "",
val confId: String = "",
val userInfo: JitsiMeetUserInfo = JitsiMeetUserInfo(),
val widget: Async<Widget> = Uninitialized
) : MvRxState

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.call.conference
import android.net.Uri
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<String, String?> by lazy {
configString?.split("&")
?.map { it.split("=") }
?.map { (key, value) -> key to value }
?.toMap()
?: mapOf()
}
}
private const val DEFAULT_JITSI_DOMAIN = "jitsi.riot.im"

View file

@ -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<out String>?, requestCode: Int, listener: PermissionListener?) {
JitsiMeetActivityDelegate.requestPermissions(this, permissions, requestCode, listener)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
JitsiMeetActivityDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onConferenceTerminated(p0: MutableMap<String, Any>?) {
finish()
}
override fun onConferenceJoined(p0: MutableMap<String, Any>?) {
}
override fun onConferenceWillJoin(p0: MutableMap<String, Any>?) {
}
companion object {
fun newIntent(context: Context, roomId: String, widgetId: String, enableVideo: Boolean): Intent {
return Intent(context, VectorJitsiActivity::class.java).apply {
putExtra(MvRx.KEY_ARG, Args(roomId, widgetId, enableVideo))
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
}
}
}

View file

@ -80,4 +80,5 @@ sealed class RoomDetailAction : VectorViewModelAction {
object SelectStickerAttachment : RoomDetailAction()
object OpenIntegrationManager: RoomDetailAction()
object ManageIntegrations: RoomDetailAction()
}

View file

@ -29,8 +29,11 @@ import android.text.Spannable
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
@ -77,6 +80,7 @@ import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.ui.views.ActiveCallView
import im.vector.app.core.ui.views.ActiveCallViewHolder
import im.vector.app.core.ui.views.ActiveConferenceView
import im.vector.app.core.ui.views.JumpToReadMarkerView
import im.vector.app.core.ui.views.NotificationAreaView
import im.vector.app.core.utils.Debouncer
@ -110,6 +114,7 @@ import im.vector.app.features.attachments.toGroupedContentAttachmentData
import im.vector.app.features.call.SharedActiveCallViewModel
import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.WebRtcPeerConnectionManager
import im.vector.app.features.call.conference.JitsiCallViewModel
import im.vector.app.features.command.Command
import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
import im.vector.app.features.crypto.util.toImageRes
@ -129,7 +134,6 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationD
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
import im.vector.app.features.home.room.detail.widget.RoomWidgetsBannerView
import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet
import im.vector.app.features.home.room.detail.widget.WidgetRequestCodes
import im.vector.app.features.html.EventHtmlRenderer
@ -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<ImageView>(R.id.action_view_icon_image)
.setColorFilter(ContextCompat.getColor(requireContext(), R.color.riotx_accent))
actionView.findViewById<TextView>(R.id.cart_badge).isVisible = true
actionView.findViewById<TextView>(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<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
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")
}

View file

@ -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()

View file

@ -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) }

View file

@ -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<List<Widget>>() {
class RoomWidgetController @Inject constructor(val stringProvider: StringProvider, val colorProvider: ColorProvider) : 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)
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) }
.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()
}
}

View file

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

View file

@ -27,6 +27,7 @@ import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.core.resources.ColorProvider
import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.RoomDetailViewModel
import im.vector.app.features.home.room.detail.RoomDetailViewState
import im.vector.app.features.navigation.Navigator
@ -77,6 +78,11 @@ class RoomWidgetsBottomSheet : VectorBaseBottomSheetDialogFragment(), RoomWidget
dismiss()
}
override fun didSelectManageWidgets() {
roomDetailViewModel.handle(RoomDetailAction.OpenIntegrationManager)
dismiss()
}
companion object {
fun newInstance(): RoomWidgetsBottomSheet {
return RoomWidgetsBottomSheet()

View file

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

View file

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

View file

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

View file

@ -0,0 +1,32 @@
<?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">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/call_connecting"
android:textColor="@android:color/white" />
<ProgressBar
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:indeterminate="true" />
</LinearLayout>
</FrameLayout>

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout style="?attr/actionButtonStyle"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:focusable="true"
xmlns:tools="http://schemas.android.com/tools">
<ImageView
android:id="@+id/action_view_icon_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:tint="@color/riotx_accent"
android:src="@drawable/ic_integrations"/>
<TextView
android:id="@+id/cart_badge"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="right|end|top"
android:layout_marginEnd="-4dp"
android:layout_marginTop="8dp"
android:background="@drawable/bg_unread_highlight"
android:gravity="center"
android:textColor="@android:color/white"
tools:text="8"
android:textSize="8sp"/>
</FrameLayout>

View file

@ -104,6 +104,14 @@
app:layout_constraintTop_toBottomOf="@id/syncStateView"
tools:visibility="visible" />
<im.vector.app.core.ui.views.ActiveConferenceView
android:id="@+id/activeConferenceView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/activeCallView"
tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
@ -112,7 +120,7 @@
app:layout_constraintBottom_toTopOf="@+id/recyclerViewBarrier"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/activeCallView"
app:layout_constraintTop_toBottomOf="@id/activeConferenceView"
tools:listitem="@layout/item_timeline_event_base" />
<FrameLayout
@ -123,15 +131,15 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/activeCallView">
<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"

View file

@ -1,9 +1,10 @@
<?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:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:minHeight="64dp"
@ -13,14 +14,36 @@
<ImageView
android:id="@+id/roomWidgetAvatar"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="8dp"
tools:src="@tools:sample/avatars" />
<TextView
app:layout_constraintStart_toEndOf="@id/roomWidgetAvatar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/roomWidgetUrl"
app:layout_constraintVertical_chainStyle="packed"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:id="@+id/roomWidgetName"
style="@style/BottomSheetItemTextMain"
tools:text="@sample/matrix.json/data/displayName" />
</LinearLayout>
<TextView
app:layout_constraintStart_toStartOf="@id/roomWidgetName"
app:layout_constraintEnd_toEndOf="@id/roomWidgetName"
app:layout_constraintTop_toBottomOf="@id/roomWidgetName"
app:layout_constraintBottom_toBottomOf="parent"
android:textStyle="normal"
android:id="@+id/roomWidgetUrl"
style="@style/BottomSheetItemTextSecondary"
tools:text="https://foobar" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,47 @@
<?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"
android:foreground="?attr/selectableItemBackground"
tools:parentTag="android.widget.RelativeLayout">
<TextView
android:id="@+id/activeConferenceInfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toStartOf="@id/returnToCallButton"
android:background="?attr/selectableItemBackground"
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" />
<TextView
android:id="@+id/returnToCallButton"
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:textAllCaps="true"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold" />
</merge>

View file

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

View file

@ -1210,6 +1210,8 @@
<string name="widget_integration_invalid_parameter">A parameter is not valid.</string>
<string name="integration_manager_not_configured">No integration manager configured.</string>
<string name="room_add_matrix_apps">Add Matrix apps</string>
<string name="room_manage_integrations">Manage Integrations</string>
<string name="room_no_active_widgets">No active widgets</string>
<string name="settings_labs_native_camera">Use native camera</string>
<string name="settings_labs_native_camera_summary">Start the system camera instead of the custom camera screen.</string>
<string name="settings_labs_keyboard_options_to_send_message">Use keyboard enter key to send message</string>

View file

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