From 8e2a1d3bcd539bd8754313c90468b8633afb1292 Mon Sep 17 00:00:00 2001
From: ganfra <francoisg@element.io>
Date: Wed, 7 Jul 2021 21:43:39 +0200
Subject: [PATCH] Jitsi call: implement RemoveJitsiWidgetView

---
 .../app/core/ui/views/ActiveConferenceView.kt | 110 ------------
 .../call/conference/RemoveJitsiWidgetView.kt  | 159 ++++++++++++++++++
 .../home/room/detail/RoomDetailFragment.kt    |  48 ++----
 .../home/room/detail/RoomDetailViewModel.kt   |  48 ++++--
 .../home/room/detail/RoomDetailViewState.kt   |   3 +-
 .../timeline/factory/WidgetItemFactory.kt     |   5 -
 .../main/res/layout/fragment_room_detail.xml  |  15 +-
 .../layout/view_active_conference_view.xml    |  43 -----
 .../res/layout/view_remove_jitsi_widget.xml   | 123 ++++++++++++++
 vector/src/main/res/values/strings.xml        |   5 +
 10 files changed, 345 insertions(+), 214 deletions(-)
 delete 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/RemoveJitsiWidgetView.kt
 delete mode 100644 vector/src/main/res/layout/view_active_conference_view.xml
 create mode 100644 vector/src/main/res/layout/view_remove_jitsi_widget.xml

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
deleted file mode 100644
index 256f2d963e..0000000000
--- a/vector/src/main/java/im/vector/app/core/ui/views/ActiveConferenceView.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (c) 2020 New Vector Ltd
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package im.vector.app.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 androidx.core.view.isVisible
-import im.vector.app.R
-import im.vector.app.core.utils.tappableMatchingText
-import im.vector.app.databinding.ViewActiveConferenceViewBinding
-import im.vector.app.features.home.room.detail.RoomDetailViewState
-import im.vector.app.features.themes.ThemeUtils
-import org.matrix.android.sdk.api.session.room.model.Membership
-import org.matrix.android.sdk.api.session.widgets.model.Widget
-import org.matrix.android.sdk.api.session.widgets.model.WidgetType
-
-class ActiveConferenceView @JvmOverloads constructor(
-        context: Context,
-        attrs: AttributeSet? = null,
-        defStyleAttr: Int = 0
-) : RelativeLayout(context, attrs, defStyleAttr) {
-
-    interface Callback {
-        fun onTapJoinAudio(jitsiWidget: Widget)
-        fun onTapJoinVideo(jitsiWidget: Widget)
-        fun onDelete(jitsiWidget: Widget)
-    }
-
-    var callback: Callback? = null
-    private var jitsiWidget: Widget? = null
-
-    private lateinit var views: ViewActiveConferenceViewBinding
-
-    init {
-        setupView()
-    }
-
-    private fun setupView() {
-        inflate(context, R.layout.view_active_conference_view, this)
-        views = ViewActiveConferenceViewBinding.bind(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)
-                }
-            }
-        })
-
-        views.activeConferenceInfo.apply {
-            text = styledText
-            movementMethod = LinkMovementMethod.getInstance()
-        }
-
-        views.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
-            jitsiWidget = state.activeRoomWidgets()?.firstOrNull {
-                // for now only jitsi?
-                it.type == WidgetType.Jitsi
-            }
-
-            isVisible = jitsiWidget != null
-            // if sent by me or if i can moderate?
-            views.deleteWidgetButton.isVisible = state.isAllowedToManageWidgets
-        } else {
-            isVisible = false
-        }
-    }
-}
diff --git a/vector/src/main/java/im/vector/app/features/call/conference/RemoveJitsiWidgetView.kt b/vector/src/main/java/im/vector/app/features/call/conference/RemoveJitsiWidgetView.kt
new file mode 100644
index 0000000000..cb26d5416b
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/call/conference/RemoveJitsiWidgetView.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (c) 2021 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.annotation.SuppressLint
+import android.content.Context
+import android.content.res.ColorStateList
+import android.util.AttributeSet
+import android.view.MotionEvent
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.ContextCompat
+import androidx.core.view.isVisible
+import androidx.core.widget.ImageViewCompat
+import im.vector.app.R
+import im.vector.app.databinding.ViewRemoveJitsiWidgetBinding
+import im.vector.app.features.home.room.detail.RoomDetailViewState
+import org.matrix.android.sdk.api.session.room.model.Membership
+
+@SuppressLint("ClickableViewAccessibility") class RemoveJitsiWidgetView @JvmOverloads constructor(
+        context: Context,
+        attrs: AttributeSet? = null,
+        defStyleAttr: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr) {
+
+    private sealed class State {
+        object Unmount : State()
+        object Idle : State()
+        data class Sliding(val initialX: Float, val translationX: Float, val hasReachedActivationThreshold: Boolean) : State()
+        object Progress : State()
+    }
+
+    private val views: ViewRemoveJitsiWidgetBinding
+    private var state: State = State.Unmount
+    var onCompleteSliding: (() -> Unit)? = null
+
+    init {
+        inflate(context, R.layout.view_remove_jitsi_widget, this)
+        views = ViewRemoveJitsiWidgetBinding.bind(this)
+        views.removeJitsiSlidingContainer.setOnTouchListener { _, event ->
+            val currentState = state
+            return@setOnTouchListener when (event.action) {
+                MotionEvent.ACTION_DOWN   -> {
+                    if (currentState == State.Idle) {
+                        val initialX = views.removeJitsiSlidingContainer.x - event.rawX
+                        updateState(State.Sliding(initialX, 0f, false))
+                    }
+                    true
+                }
+                MotionEvent.ACTION_UP,
+                MotionEvent.ACTION_CANCEL -> {
+                    if (currentState is State.Sliding) {
+                        if (currentState.hasReachedActivationThreshold) {
+                            updateState(State.Progress)
+                        } else {
+                            updateState(State.Idle)
+                        }
+                    }
+                    true
+                }
+                MotionEvent.ACTION_MOVE   -> {
+                    if (currentState is State.Sliding) {
+                        val translationX = (currentState.initialX + event.rawX).coerceAtLeast(0f)
+                        val hasReachedActivationThreshold = views.removeJitsiSlidingContainer.width + translationX >= views.removeJitsiHangupContainer.x
+                        updateState(State.Sliding(currentState.initialX, translationX, hasReachedActivationThreshold))
+                    }
+                    true
+                }
+                else                      -> false
+            }
+        }
+        renderInternalState(state)
+    }
+
+    fun render(roomDetailViewState: RoomDetailViewState) {
+        val summary = roomDetailViewState.asyncRoomSummary()
+        val newState = if (summary?.membership != Membership.JOIN || !roomDetailViewState.isAllowedToManageWidgets || roomDetailViewState.jitsiState.widgetId == null) {
+            State.Unmount
+        } else if (roomDetailViewState.jitsiState.deleteWidgetInProgress) {
+            State.Progress
+        } else {
+            State.Idle
+        }
+        // Don't force Idle if we are already sliding
+        if (state is State.Sliding && newState is State.Idle) {
+            return
+        } else {
+            updateState(newState)
+        }
+    }
+
+    private fun updateState(newState: State) {
+        if (newState == state) {
+            return
+        }
+        renderInternalState(newState)
+        state = newState
+        if (state == State.Progress) {
+            onCompleteSliding?.invoke()
+        }
+    }
+
+    private fun renderInternalState(state: State) {
+        isVisible = state != State.Unmount
+        when (state) {
+            State.Progress   -> {
+                isVisible = true
+                views.updateVisibilities(true)
+                views.updateHangupColors(true)
+            }
+            State.Idle       -> {
+                isVisible = true
+                views.updateVisibilities(false)
+                views.removeJitsiSlidingContainer.translationX = 0f
+                views.updateHangupColors(false)
+            }
+            is State.Sliding -> {
+                isVisible = true
+                views.updateVisibilities(false)
+                views.removeJitsiSlidingContainer.translationX = state.translationX
+                views.updateHangupColors(state.hasReachedActivationThreshold)
+            }
+            else             -> Unit
+        }
+    }
+
+    private fun ViewRemoveJitsiWidgetBinding.updateVisibilities(isProgress: Boolean) {
+        removeJitsiProgressContainer.isVisible = isProgress
+        removeJitsiHangupContainer.isVisible = !isProgress
+        removeJitsiSlidingContainer.isVisible = !isProgress
+    }
+
+    private fun ViewRemoveJitsiWidgetBinding.updateHangupColors(activated: Boolean) {
+        val iconTintColor: Int
+        val bgColor: Int
+        if (activated) {
+            bgColor = ContextCompat.getColor(context, R.color.palette_vermilion)
+            iconTintColor = ContextCompat.getColor(context, R.color.palette_white)
+        } else {
+            bgColor = ContextCompat.getColor(context, android.R.color.transparent)
+            iconTintColor = ContextCompat.getColor(context, R.color.palette_vermilion)
+        }
+        removeJitsiHangupContainer.setBackgroundColor(bgColor)
+        ImageViewCompat.setImageTintList(removeJitsiHangupIcon, ColorStateList.valueOf(iconTintColor))
+    }
+}
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 3e55b2b924..224c52b30f 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
@@ -67,7 +67,6 @@ import com.airbnb.mvrx.Success
 import com.airbnb.mvrx.args
 import com.airbnb.mvrx.fragmentViewModel
 import com.airbnb.mvrx.withState
-import com.facebook.react.bridge.JavaOnlyMap
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.jakewharton.rxbinding3.view.focusChanges
 import com.jakewharton.rxbinding3.widget.textChanges
@@ -90,7 +89,6 @@ import im.vector.app.core.intent.getMimeTypeFromUri
 import im.vector.app.core.platform.VectorBaseFragment
 import im.vector.app.core.platform.showOptimizedSnackbar
 import im.vector.app.core.resources.ColorProvider
-import im.vector.app.core.ui.views.ActiveConferenceView
 import im.vector.app.core.ui.views.CurrentCallsCardView
 import im.vector.app.core.ui.views.FailedMessagesWarningView
 import im.vector.app.core.ui.views.NotificationAreaView
@@ -124,7 +122,6 @@ import im.vector.app.features.call.VectorCallActivity
 import im.vector.app.features.call.conference.JitsiBroadcastEmitter
 import im.vector.app.features.call.conference.JitsiBroadcastEventObserver
 import im.vector.app.features.call.conference.JitsiCallViewModel
-import im.vector.app.features.call.conference.extractConferenceUrl
 import im.vector.app.features.call.webrtc.WebRtcCallManager
 import im.vector.app.features.command.Command
 import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
@@ -178,7 +175,6 @@ import nl.dionsegijn.konfetti.models.Shape
 import nl.dionsegijn.konfetti.models.Size
 import org.billcarsonfr.jsonviewer.JSonViewerDialog
 import org.commonmark.parser.Parser
-import org.jitsi.meet.sdk.BroadcastEmitter
 import org.jitsi.meet.sdk.BroadcastEvent
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.content.ContentAttachmentData
@@ -331,9 +327,10 @@ class RoomDetailFragment @Inject constructor(
         setupJumpToReadMarkerView()
         setupActiveCallView()
         setupJumpToBottomView()
-        setupConfBannerView()
         setupEmojiPopup()
         setupFailedMessagesWarningView()
+        setupRemoveJitsiWidgetView()
+
 
         views.roomToolbarContentView.debouncedClicks {
             navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
@@ -397,7 +394,7 @@ class RoomDetailFragment @Inject constructor(
                 RoomDetailViewEvents.OpenActiveWidgetBottomSheet         -> onViewWidgetsClicked()
                 is RoomDetailViewEvents.ShowInfoOkDialog                 -> showDialogWithMessage(it.message)
                 is RoomDetailViewEvents.JoinJitsiConference              -> joinJitsiRoom(it.widget, it.withVideo)
-                RoomDetailViewEvents.LeaveJitsiConference -> leaveJitsiConference()
+                RoomDetailViewEvents.LeaveJitsiConference                -> leaveJitsiConference()
                 RoomDetailViewEvents.ShowWaitingView                     -> vectorBaseActivity.showWaitingView()
                 RoomDetailViewEvents.HideWaitingView                     -> vectorBaseActivity.hideWaitingView()
                 is RoomDetailViewEvents.RequestNativeWidgetPermission    -> requestNativeWidgetPermission(it)
@@ -420,6 +417,18 @@ class RoomDetailFragment @Inject constructor(
         }
     }
 
+    private fun setupRemoveJitsiWidgetView() {
+        views.removeJitsiWidgetView.onCompleteSliding = {
+            withState(roomDetailViewModel) {
+                val jitsiWidgetId = it.jitsiState.widgetId ?: return@withState
+                if (it.jitsiState.hasJoined) {
+                    leaveJitsiConference()
+                }
+                roomDetailViewModel.handle(RoomDetailAction.RemoveWidget(jitsiWidgetId))
+            }
+        }
+    }
+
     private fun leaveJitsiConference() {
         JitsiBroadcastEmitter(vectorBaseActivity).emitConferenceEnded()
     }
@@ -530,31 +539,6 @@ class RoomDetailFragment @Inject constructor(
         )
     }
 
-    private fun setupConfBannerView() {
-        views.activeConferenceView.callback = object : ActiveConferenceView.Callback {
-            override fun onTapJoinAudio(jitsiWidget: Widget) {
-                // need to check if allowed first
-                roomDetailViewModel.handle(RoomDetailAction.EnsureNativeWidgetAllowed(
-                        widget = jitsiWidget,
-                        userJustAccepted = false,
-                        grantedEvents = RoomDetailViewEvents.JoinJitsiConference(jitsiWidget, false))
-                )
-            }
-
-            override fun onTapJoinVideo(jitsiWidget: Widget) {
-                roomDetailViewModel.handle(RoomDetailAction.EnsureNativeWidgetAllowed(
-                        widget = jitsiWidget,
-                        userJustAccepted = false,
-                        grantedEvents = RoomDetailViewEvents.JoinJitsiConference(jitsiWidget, true))
-                )
-            }
-
-            override fun onDelete(jitsiWidget: Widget) {
-                roomDetailViewModel.handle(RoomDetailAction.RemoveWidget(jitsiWidget.widgetId))
-            }
-        }
-    }
-
     private fun setupEmojiPopup() {
         emojiPopup = EmojiPopup
                 .Builder
@@ -1261,7 +1245,7 @@ class RoomDetailFragment @Inject constructor(
         invalidateOptionsMenu()
         val summary = state.asyncRoomSummary()
         renderToolbar(summary, state.typingMessage)
-        views.activeConferenceView.render(state)
+        views.removeJitsiWidgetView.render(state)
         views.failedMessagesWarningView.render(state.hasFailedSending)
         val inviter = state.asyncInviter()
         if (summary?.membership == Membership.JOIN) {
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 106c753068..29b50eb77e 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
@@ -60,12 +60,12 @@ import io.reactivex.Observable
 import io.reactivex.rxkotlin.subscribeBy
 import io.reactivex.schedulers.Schedulers
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import org.commonmark.parser.Parser
 import org.commonmark.renderer.html.HtmlRenderer
 import org.jitsi.meet.sdk.BroadcastEvent
-import org.jitsi.meet.sdk.JitsiMeet
 import org.matrix.android.sdk.api.MatrixCallback
 import org.matrix.android.sdk.api.MatrixPatterns
 import org.matrix.android.sdk.api.extensions.orFalse
@@ -241,18 +241,25 @@ class RoomDetailViewModel @AssistedInject constructor(
                     widgets.filter { it.isActive }
                 }
                 .execute { widgets ->
-                    val jitsiWidget = widgets()?.firstOrNull { it.type == WidgetType.Jitsi }
-                    val jitsiConfId = jitsiWidget?.let {
-                        jitsiService.extractProperties(it)?.confId
-                    }
                     copy(
                             activeRoomWidgets = widgets,
-                            jitsiState = jitsiState.copy(
-                                    confId = jitsiConfId,
-                                    widgetId = jitsiWidget?.widgetId
-                            )
                     )
                 }
+
+        asyncSubscribe(RoomDetailViewState::activeRoomWidgets) { widgets ->
+            setState {
+                val jitsiWidget = widgets.firstOrNull { it.type == WidgetType.Jitsi }
+                val jitsiConfId = jitsiWidget?.let {
+                    jitsiService.extractProperties(it)?.confId
+                }
+                copy(
+                        jitsiState = jitsiState.copy(
+                                confId = jitsiConfId,
+                                widgetId = jitsiWidget?.widgetId
+                        )
+                )
+            }
+        }
     }
 
     private fun observeMyRoomMember() {
@@ -318,8 +325,8 @@ class RoomDetailViewModel @AssistedInject constructor(
             is RoomDetailAction.ManageIntegrations               -> handleManageIntegrations()
             is RoomDetailAction.AddJitsiWidget                   -> handleAddJitsiConference(action)
             is RoomDetailAction.UpdateJoinJitsiCallStatus        -> handleJitsiCallJoinStatus(action)
-            is RoomDetailAction.JoinJitsiCall -> handleJoinJitsiCall()
-            is RoomDetailAction.LeaveJitsiCall -> handleLeaveJitsiCall()
+            is RoomDetailAction.JoinJitsiCall                    -> handleJoinJitsiCall()
+            is RoomDetailAction.LeaveJitsiCall                   -> handleLeaveJitsiCall()
             is RoomDetailAction.RemoveWidget                     -> handleDeleteWidget(action.widgetId)
             is RoomDetailAction.EnsureNativeWidgetAllowed        -> handleCheckWidgetAllowed(action)
             is RoomDetailAction.CancelSend                       -> handleCancel(action)
@@ -363,8 +370,8 @@ class RoomDetailViewModel @AssistedInject constructor(
         _viewEvents.post(RoomDetailViewEvents.LeaveJitsiConference)
     }
 
-    private fun handleJoinJitsiCall() = withState{ state ->
-        val jitsiWidget = state.activeRoomWidgets()?.firstOrNull { it.widgetId ==  state.jitsiState.widgetId} ?: return@withState
+    private fun handleJoinJitsiCall() = withState { state ->
+        val jitsiWidget = state.activeRoomWidgets()?.firstOrNull { it.widgetId == state.jitsiState.widgetId } ?: return@withState
         val action = RoomDetailAction.EnsureNativeWidgetAllowed(jitsiWidget, false, RoomDetailViewEvents.JoinJitsiConference(jitsiWidget, true))
         handleCheckWidgetAllowed(action)
     }
@@ -477,10 +484,15 @@ class RoomDetailViewModel @AssistedInject constructor(
         }
     }
 
-    private fun handleDeleteWidget(widgetId: String) {
-        _viewEvents.post(RoomDetailViewEvents.ShowWaitingView)
+    private fun handleDeleteWidget(widgetId: String) = withState { state ->
+        val isJitsiWidget = state.jitsiState.widgetId == widgetId
         viewModelScope.launch(Dispatchers.IO) {
             try {
+                if (isJitsiWidget) {
+                    setState { copy(jitsiState = jitsiState.copy(deleteWidgetInProgress = true)) }
+                } else {
+                    _viewEvents.post(RoomDetailViewEvents.ShowWaitingView)
+                }
                 session.widgetService().destroyRoomWidget(room.roomId, widgetId)
                 // local echo
                 setState {
@@ -496,7 +508,11 @@ class RoomDetailViewModel @AssistedInject constructor(
             } catch (failure: Throwable) {
                 _viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.failed_to_remove_widget)))
             } finally {
-                _viewEvents.post(RoomDetailViewEvents.HideWaitingView)
+                if (isJitsiWidget) {
+                    setState { copy(jitsiState = jitsiState.copy(deleteWidgetInProgress = false)) }
+                } else {
+                    _viewEvents.post(RoomDetailViewEvents.HideWaitingView)
+                }
             }
         }
     }
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 75650ed322..f368036b9e 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
@@ -59,7 +59,8 @@ data class JitsiState(
         val hasJoined: Boolean = false,
         // Not null if we have an active jitsi widget on the room
         val confId: String? = null,
-        val widgetId: String? = null
+        val widgetId: String? = null,
+        val deleteWidgetInProgress: Boolean = false
 )
 
 data class RoomDetailViewState(
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt
index 84867e15c6..e856907717 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt
@@ -38,13 +38,8 @@ class WidgetItemFactory @Inject constructor(
         private val avatarSizeProvider: AvatarSizeProvider,
         private val messageColorProvider: MessageColorProvider,
         private val avatarRenderer: AvatarRenderer,
-        private val activeSessionDataSource: ActiveSessionDataSource,
         private val roomSummariesHolder: RoomSummariesHolder
 ) {
-    private val currentUserId: String?
-        get() = activeSessionDataSource.currentValue?.orNull()?.myUserId
-
-    private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId
 
     fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
         val event = params.event
diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml
index 65da9156c9..3b96146c4d 100644
--- a/vector/src/main/res/layout/fragment_room_detail.xml
+++ b/vector/src/main/res/layout/fragment_room_detail.xml
@@ -100,13 +100,14 @@
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toBottomOf="@id/appBarLayout" />
 
-    <im.vector.app.core.ui.views.ActiveConferenceView
-        android:id="@+id/activeConferenceView"
+    <im.vector.app.features.call.conference.RemoveJitsiWidgetView
+        android:id="@+id/removeJitsiWidgetView"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:visibility="gone"
-        app:layout_constraintTop_toBottomOf="@id/syncStateView"
-        tools:visibility="visible" />
+        android:visibility="visible"
+        android:background="?android:colorBackground"
+        android:minHeight="54dp"
+        app:layout_constraintTop_toBottomOf="@id/syncStateView"/>
 
     <androidx.recyclerview.widget.RecyclerView
         android:id="@+id/timelineRecyclerView"
@@ -116,7 +117,7 @@
         app:layout_constraintBottom_toTopOf="@+id/timelineRecyclerViewBarrier"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/activeConferenceView"
+        app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView"
         tools:listitem="@layout/item_timeline_event_base" />
 
     <com.google.android.material.chip.Chip
@@ -132,7 +133,7 @@
         android:visibility="invisible"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/activeConferenceView"
+        app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView"
         tools:visibility="visible" />
 
 
diff --git a/vector/src/main/res/layout/view_active_conference_view.xml b/vector/src/main/res/layout/view_active_conference_view.xml
deleted file mode 100644
index 9f26ed9a1a..0000000000
--- a/vector/src/main/res/layout/view_active_conference_view.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<merge xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:background="?colorPrimary"
-    tools:parentTag="android.widget.RelativeLayout">
-
-    <TextView
-        android:id="@+id/activeConferenceInfo"
-        style="@style/Widget.Vector.TextView.Body"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_toStartOf="@id/deleteWidgetButton"
-        android:drawablePadding="10dp"
-        android:gravity="center_vertical"
-        android:paddingStart="16dp"
-        android:paddingTop="12dp"
-        android:paddingEnd="16dp"
-        android:paddingBottom="12dp"
-        android:textColor="?colorOnPrimary"
-        android:textColorLink="?colorOnPrimary"
-        app:drawableStartCompat="@drawable/ic_call_answer"
-        app:drawableTint="?colorOnPrimary"
-        tools:text="@string/ongoing_conference_call" />
-
-    <Button
-        android:id="@+id/deleteWidgetButton"
-        style="@style/Widget.Vector.Button.Text.OnPrimary"
-        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:paddingStart="8dp"
-        android:paddingEnd="16dp"
-        android:text="@string/action_close"
-        android:textStyle="bold" />
-
-</merge>
diff --git a/vector/src/main/res/layout/view_remove_jitsi_widget.xml b/vector/src/main/res/layout/view_remove_jitsi_widget.xml
new file mode 100644
index 0000000000..0da5493ce9
--- /dev/null
+++ b/vector/src/main/res/layout/view_remove_jitsi_widget.xml
@@ -0,0 +1,123 @@
+<?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:minHeight="54dp"
+    tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
+
+    <LinearLayout android:id="@+id/removeJitsiProgressContainer"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0.0"
+        android:orientation="horizontal"
+        android:visibility="gone"
+        android:gravity="center_vertical"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <ProgressBar
+            android:layout_marginStart="@dimen/layout_horizontal_margin"
+            android:indeterminateTintMode="src_atop"
+            android:indeterminateTint="?vctr_content_primary"
+            android:layout_width="16dp"
+            android:layout_height="16dp" />
+
+        <TextView
+            android:text="@string/call_remove_jitsi_widget_progress"
+            style="@style/Widget.Vector.TextView.Body"
+            android:textColor="?vctr_content_primary"
+            android:layout_marginStart="8dp"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
+
+    </LinearLayout>
+
+
+    <LinearLayout
+        android:id="@+id/removeJitsiSlidingContainer"
+        android:layout_width="wrap_content"
+        android:layout_height="0dp"
+        android:visibility="visible"
+        android:gravity="center_vertical"
+        android:orientation="horizontal"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0.0"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+
+        <TextView
+            android:id="@+id/removeJitsiSlidingTextView"
+            style="@style/Widget.Vector.TextView.Body"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="@dimen/layout_horizontal_margin"
+            android:text="@string/call_slide_to_end_conference"
+            android:textColor="?vctr_content_primary" />
+
+        <ImageView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
+            android:src="@drawable/ic_arrow_right"
+            android:tint="?vctr_content_quaternary" />
+
+        <ImageView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="8dp"
+            android:alpha="0.5"
+            android:src="@drawable/ic_arrow_right"
+            android:tint="?vctr_content_quaternary" />
+
+        <ImageView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="8dp"
+            android:alpha="0.2"
+            android:src="@drawable/ic_arrow_right"
+            android:tint="?vctr_content_quaternary" />
+
+    </LinearLayout>
+
+    <FrameLayout
+        android:id="@+id/removeJitsiHangupContainer"
+        android:layout_width="88dp"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:background="@color/vector_warning_color_2">
+
+        <ImageView
+            android:id="@+id/removeJitsiHangupIcon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:src="@drawable/ic_call_hangup" />
+
+    </FrameLayout>
+
+    <View
+        android:layout_width="0dp"
+        android:layout_height="1dp"
+        android:background="?attr/vctr_system"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <View
+        android:layout_width="0dp"
+        android:layout_height="1dp"
+        android:background="?attr/vctr_system"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent" />
+
+
+</merge>
\ No newline at end of file
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index da7ed473ff..632ecf3409 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -742,6 +742,8 @@
     <string name="call_error_camera_init_failed">Cannot initialize the camera</string>
     <string name="call_error_answered_elsewhere">call answered elsewhere</string>
 
+    <string name="call_remove_jitsi_widget_progress">Ending call…</string>
+
     <!-- medias picker string -->
     <string name="media_picker_both_capture_title">Take a picture or a video"</string>
     <string name="media_picker_cannot_record_video">Cannot record video"</string>
@@ -3247,6 +3249,8 @@
     <string name="call_transfer_transfer_to_title">Transfer to %1$s</string>
     <string name="call_transfer_unknown_person">Unknown person</string>
 
+    <string name="call_slide_to_end_conference">Slide to end the call for everyone</string>
+
     <string name="re_authentication_activity_title">Re-Authentication Needed</string>
     <!-- Note to translators: the translation MUST contain the string "${app_name}", which will be replaced by the application name -->
     <string name="template_re_authentication_default_confirm_text">${app_name} requires you to enter your credentials to perform this action.</string>
@@ -3409,4 +3413,5 @@
     <string name="teammate_spaces_arent_quite_ready">"Teammate spaces aren’t quite ready but you can still give them a try"</string>
     <string name="teammate_spaces_might_not_join">"At the moment people might not be able to join any private rooms you make.\n\nWe’ll be improving this as part of the beta, but just wanted to let you know."</string>
     <string name="error_failed_to_join_room">Sorry, an error occurred while trying to join: %s</string>
+
 </resources>