Jitsi call: implement RemoveJitsiWidgetView

This commit is contained in:
ganfra 2021-07-07 21:43:39 +02:00
parent b7e5a6cf28
commit 8e2a1d3bcd
10 changed files with 345 additions and 214 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 arent 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\nWell 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>