From 3ce1e3e5d9e546828dca1491606b8736c79966ba Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 13 Aug 2020 16:34:28 +0200 Subject: [PATCH] Add/Remove jitsi widget via option menu --- .../android/sdk/api/auth/data/WellKnown.kt | 16 ++- .../homeserver/HomeServerCapabilities.kt | 4 +- .../database/RealmSessionStoreMigration.kt | 8 ++ .../SessionRealmConfigurationFactory.kt | 2 +- .../mapper/HomeServerCapabilitiesMapper.kt | 3 +- .../model/HomeServerCapabilitiesEntity.kt | 3 +- .../DefaultGetHomeServerCapabilitiesTask.kt | 1 + .../app/core/ui/views/ActiveConferenceView.kt | 33 +++++ .../call/conference/VectorJitsiActivity.kt | 9 +- .../home/room/detail/RoomDetailAction.kt | 2 + .../home/room/detail/RoomDetailFragment.kt | 128 +++++++++++++----- .../home/room/detail/RoomDetailViewEvents.kt | 6 + .../home/room/detail/RoomDetailViewModel.kt | 100 +++++++++++++- .../home/room/detail/RoomDetailViewState.kt | 3 +- vector/src/main/res/layout/activity_jitsi.xml | 7 - .../layout/view_active_conference_view.xml | 4 +- vector/src/main/res/values/config.xml | 3 + vector/src/main/res/values/strings.xml | 9 ++ 18 files changed, 286 insertions(+), 55 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt index a4bd8badd7..b59fbacdf4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/WellKnown.kt @@ -42,6 +42,9 @@ import org.matrix.android.sdk.api.util.JsonDict * } * ] * } + * "im.vector.riot.jitsi": { + * "preferredDomain": "https://jitsi.riot.im/" + * } * } * */ @@ -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,11 @@ data class E2EWellKnownConfig( @Json(name = "default") val e2eDefault: Boolean = true ) + +@JsonClass(generateAdapter = true) +class WellKnownPreferredConfig { + + @JvmField + @Json(name = "preferredDomain") + var preferredDomain: String? = null +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt index c463fe9e72..99e02361c1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt @@ -38,7 +38,9 @@ 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, + + val preferredJitsiDomain: String? = null ) { companion object { const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 195116cfe6..badf10b3dd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -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,11 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { obj.setBoolean(HomeServerCapabilitiesEntityFields.ADMIN_E2_E_BY_DEFAULT, true) } } + + + private fun migrateTo3(realm: DynamicRealm) { + Timber.d("Step 1 -> 2") + realm.schema.get("HomeServerCapabilitiesEntity") + ?.addField(HomeServerCapabilitiesEntityFields.PREFERRED_JITSI_DOMAIN, String::class.java) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt index e53240c5b8..456eecc54a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt index 8d38c3fbe5..e5de271d93 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -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 ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt index 3b74f053b6..7e3af69436 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt index 0f9d9548d2..13ce84cf90 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt @@ -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) { 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 index d9870fac8b..34b5aa717b 100644 --- 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 @@ -24,10 +24,14 @@ 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, @@ -38,6 +42,7 @@ class ActiveConferenceView @JvmOverloads constructor( interface Callback { fun onTapJoinAudio(jitsiWidget: Widget) fun onTapJoinVideo(jitsiWidget: Widget) + fun onDelete(jitsiWidget: Widget) } var callback: Callback? = null @@ -77,5 +82,33 @@ class ActiveConferenceView @JvmOverloads constructor( text = styledText movementMethod = LinkMovementMethod.getInstance() } + + findViewById(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 = // for now only jitsi? + 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(R.id.deleteWidgetButton).isVisible = state.isAllowedToManageWidgets + } else { + isVisible = false + } } } 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 index eda284bce6..8695a746d0 100644 --- 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 @@ -20,7 +20,9 @@ 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 @@ -82,9 +84,14 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMeetActivityInterface, Ji when (viewState.widget) { is Fail -> finish() is Success -> { -// val widget = viewState.widget.invoke() + findViewById(R.id.jitsi_progress_layout).isVisible = false + jitsiMeetView?.isVisible = true configureJitsiView(viewState) } + else -> { + jitsiMeetView?.isVisible = false + findViewById(R.id.jitsi_progress_layout).isVisible = true + } } } 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 f815407816..3e29a399f0 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 @@ -81,4 +81,6 @@ sealed class RoomDetailAction : VectorViewModelAction { object SelectStickerAttachment : RoomDetailAction() object OpenIntegrationManager: RoomDetailAction() object ManageIntegrations: RoomDetailAction() + data class AddJitsiWidget(val video: Boolean): RoomDetailAction() + data class RemoveWidget(val widgetId: String): 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 c4514f6aaf..55bfcd28a6 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 @@ -356,6 +356,10 @@ class RoomDetailFragment @Inject constructor( 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() }.exhaustive } } @@ -372,15 +376,23 @@ class RoomDetailFragment @Inject constructor( 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)) + joinJitsiRoom(jitsiWidget, false) } override fun onTapJoinVideo(jitsiWidget: Widget) { - navigator.openRoomWidget(requireContext(), roomDetailArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to true)) + joinJitsiRoom(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) { navigator.openStickerPicker(this, roomDetailArgs.roomId, event.widget) } @@ -598,22 +610,7 @@ class RoomDetailFragment @Inject constructor( } 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) - } - true + handleCallRequest(item) } R.id.hangup_call -> { roomDetailViewModel.handle(RoomDetailAction.EndCall) @@ -623,6 +620,66 @@ class RoomDetailFragment @Inject constructor( } } + private fun handleCallRequest(item: MenuItem): Boolean = withState(roomDetailViewModel) { state -> + val roomSummary = state.asyncRoomSummary.invoke() ?: return@withState true + val isVideoCall = item.itemId == R.id.video_call + return@withState 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)) + } + true + } + 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) + } + true + } + 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() + } + } + true + } + } + } + private fun displayDisabledIntegrationDialog() { AlertDialog.Builder(requireActivity()) .setTitle(R.string.disabled_integration_dialog_title) @@ -915,21 +972,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) { - // 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 @@ -1167,7 +1212,7 @@ class RoomDetailFragment @Inject constructor( } } - // TimelineEventController.Callback ************************************************************ +// TimelineEventController.Callback ************************************************************ override fun onUrlClicked(url: String, title: String): Boolean { permalinkHandler @@ -1639,7 +1684,18 @@ 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() + } + +// private fun joinCurrentJitsiCall(withVideo: Boolean) { +// +// } + +// VectorInviteView.Callback override fun onAcceptInvite() { notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) @@ -1651,7 +1707,7 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.RejectInvite) } - // JumpToReadMarkerView.Callback +// JumpToReadMarkerView.Callback override fun onJumpToReadMarkerClicked() = withState(roomDetailViewModel) { jumpToReadMarkerView.isVisible = false @@ -1667,7 +1723,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)) { @@ -1688,7 +1744,7 @@ class RoomDetailFragment @Inject constructor( }.exhaustive } - // AttachmentsHelper.Callback +// AttachmentsHelper.Callback override fun onContentAttachmentsReady(attachments: List) { if (roomDetailViewModel.preventAttachmentPreview) { 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 89b42f4fc9..7d0df5288c 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 @@ -35,9 +35,15 @@ 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, 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 de63fb04e1..8f631a8a81 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 @@ -43,6 +43,7 @@ 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 org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixPatterns @@ -91,8 +92,12 @@ 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.session.widgets.model.Widget +import org.matrix.android.sdk.api.session.widgets.model.WidgetType +import org.matrix.android.sdk.internal.util.awaitCallback 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,7 +278,9 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() is RoomDetailAction.StartCall -> handleStartCall(action) is RoomDetailAction.EndCall -> handleEndCall() - is RoomDetailAction.ManageIntegrations -> handleManageIntegrations() + is RoomDetailAction.ManageIntegrations -> handleManageIntegrations() + is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action) + is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId) }.exhaustive } @@ -317,6 +328,89 @@ class RoomDetailViewModel @AssistedInject constructor( } } + private fun handleAddJitsiConference(action: RoomDetailAction.AddJitsiWidget) { + _viewEvents.post(RoomDetailViewEvents.ShowWaitingView) + viewModelScope.launch(Dispatchers.IO) { + // Build data for a jitsi widget + + // 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 + + // 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) + + //DO NOT COMMIT + 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.video}" + + "&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.video + ), + "creatorUserId" to session.myUserId, + "id" to widgetId, + "name" to "jitsi" + ) + + try { + val widget = awaitCallback { + session.widgetService().createRoomWidget(roomId, widgetId, widgetEventContent, it) + } + _viewEvents.post(RoomDetailViewEvents.JoinJitsiConference(widget, action.video)) + } 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 { 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 startTrackingUnreadMessages() { trackUnreadMessages.set(true) setState { copy(canShowJumpToReadMarker = false) } @@ -430,7 +524,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 } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index 39617639af..c2bffa4362 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -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) diff --git a/vector/src/main/res/layout/activity_jitsi.xml b/vector/src/main/res/layout/activity_jitsi.xml index 8928d298b3..e0359d220d 100644 --- a/vector/src/main/res/layout/activity_jitsi.xml +++ b/vector/src/main/res/layout/activity_jitsi.xml @@ -15,16 +15,9 @@ android:gravity="center" android:orientation="vertical"> - - diff --git a/vector/src/main/res/layout/view_active_conference_view.xml b/vector/src/main/res/layout/view_active_conference_view.xml index 4195c227a6..abe7c87080 100644 --- a/vector/src/main/res/layout/view_active_conference_view.xml +++ b/vector/src/main/res/layout/view_active_conference_view.xml @@ -12,7 +12,7 @@ android:id="@+id/activeConferenceInfo" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_toStartOf="@id/returnToCallButton" + android:layout_toStartOf="@id/deleteWidgetButton" android:background="?attr/selectableItemBackground" android:drawableStart="@drawable/ic_call" android:drawablePadding="10dp" @@ -27,7 +27,7 @@ tools:text="@string/ongoing_conference_call" /> im.vector.app.android + + jitsi.riot.im + matrix.org diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 2613166e1d..9651553fc4 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -89,8 +89,17 @@ "Due to missing permissions, some features may be missing… "Due to missing permissions, this action is not possible. You need permission to invite to start a conference in this room + You do not have permission to start a conference call in this room + A conference is already in progress! + Start video meeting + Start audio meeting + Meetings use Jisti security and permission policies. All people currently in the room will see an invite to join while your meeting is happening. Cannot start call + You cannot place a call with yourself + You cannot place a call with yourself, wait for participants to accept invitation Session information + Failed to add widget + Failed to remove widget Conference calls are not supported in encrypted rooms Call Anyway Send Anyway