mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-16 02:05:06 +08:00
Merge pull request #3863 from vector-im/feature/fga/new_voip_design
Feature/fga/new voip design
This commit is contained in:
commit
5b908c404e
@ -44,6 +44,8 @@ allprojects {
|
||||
includeGroupByRegex 'com\\.github\\.chrisbanes'
|
||||
// PFLockScreen-Android
|
||||
includeGroupByRegex 'com\\.github\\.vector-im'
|
||||
// DraggableView
|
||||
includeGroupByRegex 'com\\.github\\.hyuwah'
|
||||
|
||||
// Chat effects
|
||||
includeGroupByRegex 'com\\.github\\.jetradarmobile'
|
||||
|
1
changelog.d/3599.feature
Normal file
1
changelog.d/3599.feature
Normal file
@ -0,0 +1 @@
|
||||
New call designs
|
@ -28,6 +28,7 @@
|
||||
<!-- Other useful color -->
|
||||
<!-- Emoji text has to use a black text color -->
|
||||
<color name="emoji_color">@android:color/black</color>
|
||||
<color name="join_conference_animated_color">#0BAC7E</color>
|
||||
|
||||
<color name="half_transparent_status_bar">#80000000</color>
|
||||
|
||||
|
@ -28,6 +28,10 @@
|
||||
<dimen name="pill_min_height">20dp</dimen>
|
||||
<dimen name="pill_text_padding">4dp</dimen>
|
||||
|
||||
<dimen name="call_pip_height">128dp</dimen>
|
||||
<dimen name="call_pip_width">88dp</dimen>
|
||||
<dimen name="call_pip_radius">8dp</dimen>
|
||||
|
||||
|
||||
<dimen name="item_form_min_height">76dp</dimen>
|
||||
|
||||
|
@ -409,6 +409,7 @@ dependencies {
|
||||
implementation "androidx.autofill:autofill:$autofill_version"
|
||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12'
|
||||
implementation 'com.github.hyuwah:DraggableView:1.0.0'
|
||||
implementation 'com.github.Armen101:AudioRecordView:1.0.5'
|
||||
|
||||
// Custom Tab
|
||||
|
@ -271,7 +271,11 @@
|
||||
android:name=".features.attachments.preview.AttachmentsPreviewActivity"
|
||||
android:theme="@style/Theme.Vector.Black.AttachmentsPreview" />
|
||||
<activity
|
||||
android:supportsPictureInPicture="true"
|
||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
|
||||
android:name=".features.call.VectorCallActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=".features.call.VectorCallActivity"
|
||||
android:excludeFromRecents="true" />
|
||||
<!-- PIP Support https://developer.android.com/guide/topics/ui/picture-in-picture -->
|
||||
<activity
|
||||
|
@ -194,6 +194,37 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
|
||||
</pre>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<b>hyuwah/DraggableView</b>
|
||||
<br/>
|
||||
hyuwah/DraggableView is licensed under the MIT License
|
||||
Copyright (c) 2018 Muhammad Wahyudin
|
||||
</li>
|
||||
</ul>
|
||||
<pre>
|
||||
|
||||
Copyright (c) 2018 Muhammad Wahyudin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
</pre>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<b>com.github.piasy:BigImageViewer</b>
|
||||
|
@ -31,6 +31,7 @@ import im.vector.app.core.network.WifiDetector
|
||||
import im.vector.app.core.pushers.PushersManager
|
||||
import im.vector.app.core.utils.AssetReader
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.features.call.conference.JitsiActiveConferenceHolder
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import im.vector.app.features.configuration.VectorConfiguration
|
||||
import im.vector.app.features.crypto.keysrequest.KeyRequestHandler
|
||||
@ -39,7 +40,6 @@ import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.CurrentSpaceSuggestedRoomListDataSource
|
||||
import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
|
||||
import im.vector.app.features.html.EventHtmlRenderer
|
||||
import im.vector.app.features.html.VectorHtmlCompressor
|
||||
import im.vector.app.features.invite.AutoAcceptInvites
|
||||
@ -165,7 +165,7 @@ interface VectorComponent {
|
||||
|
||||
fun webRtcCallManager(): WebRtcCallManager
|
||||
|
||||
fun roomSummaryHolder(): RoomSummariesHolder
|
||||
fun jitsiActiveConferenceHolder(): JitsiActiveConferenceHolder
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
|
@ -98,13 +98,10 @@ class CallRingPlayerOutgoing(
|
||||
private var player: MediaPlayer? = null
|
||||
|
||||
fun start() {
|
||||
val audioManager: AudioManager? = applicationContext.getSystemService()
|
||||
applicationContext.getSystemService<AudioManager>()?.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||
player?.release()
|
||||
player = createPlayer()
|
||||
|
||||
// Check if sound is enabled
|
||||
val ringerMode = audioManager?.ringerMode
|
||||
if (player != null && ringerMode == AudioManager.RINGER_MODE_NORMAL) {
|
||||
if (player != null) {
|
||||
try {
|
||||
if (player?.isPlaying == false) {
|
||||
player?.start()
|
||||
@ -116,8 +113,6 @@ class CallRingPlayerOutgoing(
|
||||
Timber.e(failure, "## VOIP Failed to start ringing outgoing")
|
||||
player = null
|
||||
}
|
||||
} else {
|
||||
Timber.v("## VOIP Can't play $player ode $ringerMode")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -18,7 +18,9 @@ package im.vector.app.core.ui.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.RelativeLayout
|
||||
import android.util.TypedValue
|
||||
import android.widget.FrameLayout
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import im.vector.app.R
|
||||
import im.vector.app.databinding.ViewCurrentCallsBinding
|
||||
import im.vector.app.features.call.webrtc.WebRtcCall
|
||||
@ -29,7 +31,7 @@ class CurrentCallsView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : RelativeLayout(context, attrs, defStyleAttr) {
|
||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
interface Callback {
|
||||
fun onTapToReturnToCall()
|
||||
@ -42,25 +44,33 @@ class CurrentCallsView @JvmOverloads constructor(
|
||||
inflate(context, R.layout.view_current_calls, this)
|
||||
views = ViewCurrentCallsBinding.bind(this)
|
||||
setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorPrimary))
|
||||
val outValue = TypedValue().also {
|
||||
context.theme.resolveAttribute(android.R.attr.selectableItemBackground, it, true)
|
||||
}
|
||||
foreground = AppCompatResources.getDrawable(context, outValue.resourceId)
|
||||
setOnClickListener { callback?.onTapToReturnToCall() }
|
||||
}
|
||||
|
||||
fun render(calls: List<WebRtcCall>, formattedDuration: String) {
|
||||
val connectedCalls = calls.filter {
|
||||
it.mxCall.state is CallState.Connected
|
||||
}
|
||||
val heldCalls = connectedCalls.filter {
|
||||
it.isLocalOnHold || it.remoteOnHold
|
||||
}
|
||||
if (connectedCalls.isEmpty()) return
|
||||
views.currentCallsInfo.text = if (connectedCalls.size == heldCalls.size) {
|
||||
resources.getQuantityString(R.plurals.call_only_paused, heldCalls.size, heldCalls.size)
|
||||
} else {
|
||||
if (heldCalls.isEmpty()) {
|
||||
resources.getString(R.string.call_only_active, formattedDuration)
|
||||
} else {
|
||||
resources.getQuantityString(R.plurals.call_one_active_and_other_paused, heldCalls.size, formattedDuration, heldCalls.size)
|
||||
val tapToReturnFormat = if (calls.size == 1) {
|
||||
val firstCall = calls.first()
|
||||
when (firstCall.mxCall.state) {
|
||||
is CallState.Idle,
|
||||
is CallState.CreateOffer,
|
||||
is CallState.LocalRinging,
|
||||
is CallState.Dialing -> {
|
||||
resources.getString(R.string.call_ringing)
|
||||
}
|
||||
is CallState.Answering -> {
|
||||
resources.getString(R.string.call_connecting)
|
||||
}
|
||||
else -> {
|
||||
resources.getString(R.string.call_one_active, formattedDuration)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resources.getQuantityString(R.plurals.call_active_status, calls.size, calls.size)
|
||||
}
|
||||
views.currentCallsInfo.text = resources.getString(R.string.call_tap_to_return, tapToReturnFormat)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.core.ui.views
|
||||
|
||||
import androidx.core.view.isVisible
|
||||
import im.vector.app.features.call.webrtc.WebRtcCall
|
||||
|
||||
class CurrentCallsViewPresenter {
|
||||
|
||||
private var currentCallsView: CurrentCallsView? = null
|
||||
private var currentCall: WebRtcCall? = null
|
||||
private var calls: List<WebRtcCall> = emptyList()
|
||||
|
||||
private val tickListener = object : WebRtcCall.Listener {
|
||||
override fun onTick(formattedDuration: String) {
|
||||
currentCallsView?.render(calls, formattedDuration)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCall(currentCall: WebRtcCall?, calls: List<WebRtcCall>) {
|
||||
this.currentCall?.removeListener(tickListener)
|
||||
this.currentCall = currentCall
|
||||
this.currentCall?.addListener(tickListener)
|
||||
this.calls = calls
|
||||
val hasActiveCall = currentCall != null
|
||||
currentCallsView?.isVisible = hasActiveCall
|
||||
currentCallsView?.render(calls, currentCall?.formattedDuration() ?: "")
|
||||
}
|
||||
|
||||
fun bind(activeCallView: CurrentCallsView, interactionListener: CurrentCallsView.Callback) {
|
||||
this.currentCallsView = activeCallView
|
||||
this.currentCallsView?.callback = interactionListener
|
||||
this.currentCall?.addListener(tickListener)
|
||||
}
|
||||
|
||||
fun unBind() {
|
||||
this.currentCallsView?.callback = null
|
||||
this.currentCall?.removeListener(tickListener)
|
||||
currentCallsView = null
|
||||
}
|
||||
}
|
@ -1,114 +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 androidx.core.view.isVisible
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.features.call.utils.EglUtils
|
||||
import im.vector.app.features.call.webrtc.WebRtcCall
|
||||
import org.matrix.android.sdk.api.session.call.CallState
|
||||
import org.webrtc.RendererCommon
|
||||
import org.webrtc.SurfaceViewRenderer
|
||||
|
||||
class KnownCallsViewHolder {
|
||||
|
||||
private var activeCallPiP: SurfaceViewRenderer? = null
|
||||
private var currentCallsView: CurrentCallsView? = null
|
||||
private var pipWrapper: MaterialCardView? = null
|
||||
private var currentCall: WebRtcCall? = null
|
||||
private var calls: List<WebRtcCall> = emptyList()
|
||||
|
||||
private var activeCallPipInitialized = false
|
||||
|
||||
private val tickListener = object : WebRtcCall.Listener {
|
||||
override fun onTick(formattedDuration: String) {
|
||||
currentCallsView?.render(calls, formattedDuration)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCall(currentCall: WebRtcCall?, calls: List<WebRtcCall>) {
|
||||
activeCallPiP?.let {
|
||||
this.currentCall?.detachRenderers(listOf(it))
|
||||
}
|
||||
this.currentCall?.removeListener(tickListener)
|
||||
this.currentCall = currentCall
|
||||
this.currentCall?.addListener(tickListener)
|
||||
this.calls = calls
|
||||
val hasActiveCall = currentCall?.mxCall?.state is CallState.Connected
|
||||
if (hasActiveCall) {
|
||||
val isVideoCall = currentCall?.mxCall?.isVideoCall == true
|
||||
if (isVideoCall) initIfNeeded()
|
||||
currentCallsView?.isVisible = !isVideoCall
|
||||
currentCallsView?.render(calls, currentCall?.formattedDuration() ?: "")
|
||||
pipWrapper?.isVisible = isVideoCall
|
||||
activeCallPiP?.isVisible = isVideoCall
|
||||
activeCallPiP?.let {
|
||||
currentCall?.attachViewRenderers(null, it, null)
|
||||
}
|
||||
} else {
|
||||
currentCallsView?.isVisible = false
|
||||
activeCallPiP?.isVisible = false
|
||||
pipWrapper?.isVisible = false
|
||||
activeCallPiP?.let {
|
||||
currentCall?.detachRenderers(listOf(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initIfNeeded() {
|
||||
if (!activeCallPipInitialized && activeCallPiP != null) {
|
||||
activeCallPiP?.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL)
|
||||
EglUtils.rootEglBase?.let { eglBase ->
|
||||
activeCallPiP?.init(eglBase.eglBaseContext, null)
|
||||
activeCallPiP?.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_BALANCED)
|
||||
activeCallPiP?.setEnableHardwareScaler(true /* enabled */)
|
||||
activeCallPiP?.setZOrderMediaOverlay(true)
|
||||
activeCallPipInitialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(activeCallPiP: SurfaceViewRenderer,
|
||||
activeCallView: CurrentCallsView,
|
||||
pipWrapper: MaterialCardView,
|
||||
interactionListener: CurrentCallsView.Callback) {
|
||||
this.activeCallPiP = activeCallPiP
|
||||
this.currentCallsView = activeCallView
|
||||
this.pipWrapper = pipWrapper
|
||||
this.currentCallsView?.callback = interactionListener
|
||||
pipWrapper.onClick {
|
||||
interactionListener.onTapToReturnToCall()
|
||||
}
|
||||
this.currentCall?.addListener(tickListener)
|
||||
}
|
||||
|
||||
fun unBind() {
|
||||
activeCallPiP?.let {
|
||||
currentCall?.detachRenderers(listOf(it))
|
||||
}
|
||||
if (activeCallPipInitialized) {
|
||||
activeCallPiP?.release()
|
||||
}
|
||||
this.currentCallsView?.callback = null
|
||||
this.currentCall?.removeListener(tickListener)
|
||||
pipWrapper?.setOnClickListener(null)
|
||||
activeCallPiP = null
|
||||
currentCallsView = null
|
||||
pipWrapper = null
|
||||
}
|
||||
}
|
@ -23,13 +23,9 @@ import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.app.databinding.BottomSheetCallControlsBinding
|
||||
import im.vector.app.features.call.audio.CallAudioManager
|
||||
|
||||
import me.gujun.android.span.span
|
||||
|
||||
class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetCallControlsBinding>() {
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetCallControlsBinding {
|
||||
@ -45,10 +41,6 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC
|
||||
renderState(it)
|
||||
}
|
||||
|
||||
views.callControlsSoundDevice.views.bottomSheetActionClickableZone.debouncedClicks {
|
||||
callViewModel.handle(VectorCallViewActions.SwitchSoundDevice)
|
||||
}
|
||||
|
||||
views.callControlsSwitchCamera.views.bottomSheetActionClickableZone.debouncedClicks {
|
||||
callViewModel.handle(VectorCallViewActions.ToggleCamera)
|
||||
dismiss()
|
||||
@ -72,74 +64,11 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC
|
||||
callViewModel.handle(VectorCallViewActions.InitiateCallTransfer)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
callViewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is VectorCallViewEvents.ShowSoundDeviceChooser -> {
|
||||
showSoundDeviceChooser(it.available, it.current)
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSoundDeviceChooser(available: Set<CallAudioManager.Device>, current: CallAudioManager.Device) {
|
||||
val soundDevices = available.map {
|
||||
when (it) {
|
||||
CallAudioManager.Device.WIRELESS_HEADSET -> span {
|
||||
text = getString(R.string.sound_device_wireless_headset)
|
||||
textStyle = if (current == it) "bold" else "normal"
|
||||
}
|
||||
CallAudioManager.Device.PHONE -> span {
|
||||
text = getString(R.string.sound_device_phone)
|
||||
textStyle = if (current == it) "bold" else "normal"
|
||||
}
|
||||
CallAudioManager.Device.SPEAKER -> span {
|
||||
text = getString(R.string.sound_device_speaker)
|
||||
textStyle = if (current == it) "bold" else "normal"
|
||||
}
|
||||
CallAudioManager.Device.HEADSET -> span {
|
||||
text = getString(R.string.sound_device_headset)
|
||||
textStyle = if (current == it) "bold" else "normal"
|
||||
}
|
||||
}
|
||||
}
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setItems(soundDevices.toTypedArray()) { d, n ->
|
||||
d.cancel()
|
||||
when (soundDevices[n].toString()) {
|
||||
// TODO Make an adapter and handle multiple Bluetooth headsets. Also do not use translations.
|
||||
getString(R.string.sound_device_phone) -> {
|
||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.PHONE))
|
||||
}
|
||||
getString(R.string.sound_device_speaker) -> {
|
||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.SPEAKER))
|
||||
}
|
||||
getString(R.string.sound_device_headset) -> {
|
||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.HEADSET))
|
||||
}
|
||||
getString(R.string.sound_device_wireless_headset) -> {
|
||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.WIRELESS_HEADSET))
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun renderState(state: VectorCallViewState) {
|
||||
views.callControlsSoundDevice.title = getString(R.string.call_select_sound_device)
|
||||
views.callControlsSoundDevice.subTitle = when (state.device) {
|
||||
CallAudioManager.Device.PHONE -> getString(R.string.sound_device_phone)
|
||||
CallAudioManager.Device.SPEAKER -> getString(R.string.sound_device_speaker)
|
||||
CallAudioManager.Device.HEADSET -> getString(R.string.sound_device_headset)
|
||||
CallAudioManager.Device.WIRELESS_HEADSET -> getString(R.string.sound_device_wireless_headset)
|
||||
}
|
||||
|
||||
views.callControlsSwitchCamera.isVisible = state.isVideoCall && state.canSwitchCamera
|
||||
views.callControlsSwitchCamera.subTitle = getString(if (state.isFrontCamera) R.string.call_camera_front else R.string.call_camera_back)
|
||||
|
||||
if (state.isVideoCall) {
|
||||
views.callControlsToggleSDHD.isVisible = true
|
||||
if (state.isHD) {
|
||||
|
@ -36,16 +36,19 @@ class CallControlsView @JvmOverloads constructor(
|
||||
init {
|
||||
inflate(context, R.layout.view_call_controls, this)
|
||||
views = ViewCallControlsBinding.bind(this)
|
||||
|
||||
views.audioSettingsIcon.setOnClickListener { didTapAudioSettings() }
|
||||
views.ringingControlAccept.setOnClickListener { acceptIncomingCall() }
|
||||
views.ringingControlDecline.setOnClickListener { declineIncomingCall() }
|
||||
views.endCallIcon.setOnClickListener { endOngoingCall() }
|
||||
views.muteIcon.setOnClickListener { toggleMute() }
|
||||
views.videoToggleIcon.setOnClickListener { toggleVideo() }
|
||||
views.openChatIcon.setOnClickListener { returnToChat() }
|
||||
views.moreIcon.setOnClickListener { moreControlOption() }
|
||||
}
|
||||
|
||||
private fun didTapAudioSettings() {
|
||||
interactionListener?.didTapAudioSettings()
|
||||
}
|
||||
|
||||
private fun acceptIncomingCall() {
|
||||
interactionListener?.didAcceptIncomingCall()
|
||||
}
|
||||
@ -66,10 +69,6 @@ class CallControlsView @JvmOverloads constructor(
|
||||
interactionListener?.didTapToggleVideo()
|
||||
}
|
||||
|
||||
private fun returnToChat() {
|
||||
interactionListener?.returnToChat()
|
||||
}
|
||||
|
||||
private fun moreControlOption() {
|
||||
interactionListener?.didTapMore()
|
||||
}
|
||||
@ -77,49 +76,36 @@ class CallControlsView @JvmOverloads constructor(
|
||||
fun updateForState(state: VectorCallViewState) {
|
||||
val callState = state.callState.invoke()
|
||||
if (state.isAudioMuted) {
|
||||
views.muteIcon.setImageResource(R.drawable.ic_microphone_off)
|
||||
views.muteIcon.setImageResource(R.drawable.ic_mic_off)
|
||||
views.muteIcon.contentDescription = resources.getString(R.string.a11y_unmute_microphone)
|
||||
} else {
|
||||
views.muteIcon.setImageResource(R.drawable.ic_microphone_on)
|
||||
views.muteIcon.setImageResource(R.drawable.ic_mic_on)
|
||||
views.muteIcon.contentDescription = resources.getString(R.string.a11y_mute_microphone)
|
||||
}
|
||||
if (state.isVideoEnabled) {
|
||||
views.videoToggleIcon.setImageResource(R.drawable.ic_video)
|
||||
views.videoToggleIcon.contentDescription = resources.getString(R.string.a11y_stop_camera)
|
||||
} else {
|
||||
views.videoToggleIcon.setImageResource(R.drawable.ic_video_off)
|
||||
views.videoToggleIcon.setImageResource(R.drawable.ic_video_off)
|
||||
views.videoToggleIcon.contentDescription = resources.getString(R.string.a11y_start_camera)
|
||||
}
|
||||
|
||||
when (callState) {
|
||||
is CallState.Idle,
|
||||
is CallState.Dialing,
|
||||
is CallState.Answering -> {
|
||||
views.ringingControls.isVisible = true
|
||||
views.ringingControlAccept.isVisible = false
|
||||
views.ringingControlDecline.isVisible = true
|
||||
views.connectedControls.isVisible = false
|
||||
}
|
||||
is CallState.LocalRinging -> {
|
||||
views.ringingControls.isVisible = true
|
||||
views.ringingControlAccept.isVisible = true
|
||||
views.ringingControlDecline.isVisible = true
|
||||
views.connectedControls.isVisible = false
|
||||
}
|
||||
is CallState.Connected -> {
|
||||
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
|
||||
views.ringingControls.isVisible = false
|
||||
views.connectedControls.isVisible = true
|
||||
views.videoToggleIcon.isVisible = state.isVideoCall
|
||||
} else {
|
||||
views.ringingControls.isVisible = true
|
||||
views.ringingControlAccept.isVisible = false
|
||||
views.ringingControlDecline.isVisible = true
|
||||
views.connectedControls.isVisible = false
|
||||
}
|
||||
is CallState.Connected,
|
||||
is CallState.Dialing,
|
||||
is CallState.Answering -> {
|
||||
views.ringingControls.isVisible = false
|
||||
views.connectedControls.isVisible = true
|
||||
views.videoToggleIcon.isVisible = state.isVideoCall
|
||||
views.moreIcon.isVisible = callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED
|
||||
}
|
||||
is CallState.Ended,
|
||||
null -> {
|
||||
else -> {
|
||||
views.ringingControls.isVisible = false
|
||||
views.connectedControls.isVisible = false
|
||||
}
|
||||
@ -127,12 +113,12 @@ class CallControlsView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
interface InteractionListener {
|
||||
fun didTapAudioSettings()
|
||||
fun didAcceptIncomingCall()
|
||||
fun didDeclineIncomingCall()
|
||||
fun didEndCall()
|
||||
fun didTapToggleMute()
|
||||
fun didTapToggleVideo()
|
||||
fun returnToChat()
|
||||
fun didTapMore()
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.call
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.airbnb.epoxy.SimpleEpoxyController
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import im.vector.app.core.epoxy.bottomsheet.BottomSheetActionItem_
|
||||
import im.vector.app.core.extensions.configureWith
|
||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.app.databinding.BottomSheetGenericListBinding
|
||||
import im.vector.app.features.call.audio.CallAudioManager
|
||||
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet
|
||||
|
||||
class CallSoundDeviceChooserBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetGenericListBinding>() {
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetGenericListBinding {
|
||||
return BottomSheetGenericListBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
private val callViewModel: VectorCallViewModel by activityViewModel()
|
||||
private val controller = SimpleEpoxyController()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
views.bottomSheetRecyclerView.configureWith(controller, hasFixedSize = false)
|
||||
callViewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is VectorCallViewEvents.ShowSoundDeviceChooser -> {
|
||||
render(it.available, it.current)
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
callViewModel.handle(VectorCallViewActions.SwitchSoundDevice)
|
||||
}
|
||||
|
||||
private fun render(available: Set<CallAudioManager.Device>, current: CallAudioManager.Device) {
|
||||
val models = available.map { device ->
|
||||
val title = when (device) {
|
||||
is CallAudioManager.Device.WirelessHeadset -> device.name ?: getString(device.titleRes)
|
||||
else -> getString(device.titleRes)
|
||||
}
|
||||
BottomSheetActionItem_().apply {
|
||||
id(device.titleRes)
|
||||
text(title)
|
||||
iconRes(device.drawableRes)
|
||||
selected(current == device)
|
||||
listener {
|
||||
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(device))
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
controller.setModels(models)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(): RoomListQuickActionsBottomSheet {
|
||||
return RoomListQuickActionsBottomSheet()
|
||||
}
|
||||
}
|
||||
}
|
@ -32,14 +32,12 @@ class SharedKnownCallsViewModel @Inject constructor(
|
||||
val callListener = object : WebRtcCall.Listener {
|
||||
|
||||
override fun onStateUpdate(call: MxCall) {
|
||||
// post it-self
|
||||
liveKnownCalls.postValue(liveKnownCalls.value)
|
||||
liveKnownCalls.postValue(callManager.getCalls())
|
||||
}
|
||||
|
||||
override fun onHoldUnhold() {
|
||||
super.onHoldUnhold()
|
||||
// post it-self
|
||||
liveKnownCalls.postValue(liveKnownCalls.value)
|
||||
liveKnownCalls.postValue(callManager.getCalls())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,13 +17,17 @@
|
||||
package im.vector.app.features.call
|
||||
|
||||
import android.app.KeyguardManager
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.util.Rational
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.annotation.StringRes
|
||||
@ -35,9 +39,11 @@ import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.viewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.di.ScreenComponent
|
||||
import im.vector.app.core.extensions.setTextOrHide
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL
|
||||
import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL
|
||||
@ -52,6 +58,8 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.RoomDetailActivity
|
||||
import im.vector.app.features.home.room.detail.RoomDetailArgs
|
||||
import io.github.hyuwah.draggableviewlib.DraggableView
|
||||
import io.github.hyuwah.draggableviewlib.setupDraggable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
@ -87,7 +95,6 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||
}
|
||||
|
||||
private val callViewModel: VectorCallViewModel by viewModel()
|
||||
private lateinit var callArgs: CallArgs
|
||||
|
||||
@Inject lateinit var callManager: WebRtcCallManager
|
||||
@Inject lateinit var viewModelFactory: VectorCallViewModel.Factory
|
||||
@ -99,6 +106,8 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||
}
|
||||
|
||||
private var rootEglBase: EglBase? = null
|
||||
private var pipDraggrableView: DraggableView<MaterialCardView>? = null
|
||||
private var otherCallDraggableView: DraggableView<MaterialCardView>? = null
|
||||
|
||||
var surfaceRenderersAreInitialized = false
|
||||
|
||||
@ -115,13 +124,6 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||
window.navigationBarColor = Color.BLACK
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (intent.hasExtra(MvRx.KEY_ARG)) {
|
||||
callArgs = intent.getParcelableExtra(MvRx.KEY_ARG)!!
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).e("missing callArgs for VectorCall Activity")
|
||||
finish()
|
||||
}
|
||||
|
||||
Timber.tag(loggerTag.value).v("EXTRA_MODE is ${intent.getStringExtra(EXTRA_MODE)}")
|
||||
if (intent.getStringExtra(EXTRA_MODE) == INCOMING_RINGING) {
|
||||
turnScreenOnAndKeyguardOff()
|
||||
@ -129,6 +131,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||
if (savedInstanceState != null) {
|
||||
(supportFragmentManager.findFragmentByTag(FRAGMENT_DIAL_PAD_TAG) as? CallDialPadBottomSheet)?.callback = dialPadCallback
|
||||
}
|
||||
setSupportActionBar(views.callToolbar)
|
||||
configureCallViews()
|
||||
|
||||
callViewModel.subscribe(this) {
|
||||
@ -149,25 +152,89 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||
}
|
||||
.disposeOnDestroy()
|
||||
|
||||
if (callArgs.isVideoCall) {
|
||||
if (checkPermissions(PERMISSIONS_FOR_VIDEO_IP_CALL, this, permissionCameraLauncher, R.string.permissions_rationale_msg_camera_and_audio)) {
|
||||
start()
|
||||
}
|
||||
} else {
|
||||
if (checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL, this, permissionCameraLauncher, R.string.permissions_rationale_msg_record_audio)) {
|
||||
start()
|
||||
callViewModel.selectSubscribe(this, VectorCallViewState::callId, VectorCallViewState::isVideoCall) { _, isVideoCall ->
|
||||
if (isVideoCall) {
|
||||
if (checkPermissions(PERMISSIONS_FOR_VIDEO_IP_CALL, this, permissionCameraLauncher, R.string.permissions_rationale_msg_camera_and_audio)) {
|
||||
setupRenderersIfNeeded()
|
||||
}
|
||||
} else {
|
||||
if (checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL, this, permissionCameraLauncher, R.string.permissions_rationale_msg_record_audio)) {
|
||||
setupRenderersIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
intent?.takeIf { it.hasExtra(MvRx.KEY_ARG) }
|
||||
?.let { intent.getParcelableExtra<CallArgs>(MvRx.KEY_ARG) }
|
||||
?.let {
|
||||
callViewModel.handle(VectorCallViewActions.SwitchCall(it))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMenuRes() = R.menu.vector_call
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
enterPictureInPictureIfRequired()
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (!enterPictureInPictureIfRequired()) {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
private fun enterPictureInPictureIfRequired(): Boolean = withState(callViewModel) {
|
||||
if (!it.isVideoCall) {
|
||||
false
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val aspectRatio = Rational(resources.getDimensionPixelSize(R.dimen.call_pip_width), resources.getDimensionPixelSize(R.dimen.call_pip_height))
|
||||
val params = PictureInPictureParams.Builder()
|
||||
.setAspectRatio(aspectRatio)
|
||||
.build()
|
||||
renderPiPMode(it)
|
||||
enterPictureInPictureMode(params)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun isInPictureInPictureModeSafe(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInPictureInPictureMode
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) = withState(callViewModel) {
|
||||
renderState(it)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.menu_call_open_chat) {
|
||||
returnToChat()
|
||||
return true
|
||||
} else if (item.itemId == android.R.id.home) {
|
||||
// We check here as we want PiP in some cases
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
callManager.getCallById(callArgs.callId)?.detachRenderers(listOf(views.pipRenderer, views.fullscreenRenderer))
|
||||
detachRenderersIfNeeded()
|
||||
turnScreenOffAndKeyguardOn()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun detachRenderersIfNeeded() {
|
||||
val callId = withState(callViewModel) { it.callId }
|
||||
callManager.getCallById(callId)?.detachRenderers(listOf(views.pipRenderer, views.fullscreenRenderer))
|
||||
if (surfaceRenderersAreInitialized) {
|
||||
views.pipRenderer.release()
|
||||
views.fullscreenRenderer.release()
|
||||
surfaceRenderersAreInitialized = false
|
||||
}
|
||||
turnScreenOffAndKeyguardOn()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun renderState(state: VectorCallViewState) {
|
||||
@ -176,53 +243,57 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||
finish()
|
||||
return
|
||||
}
|
||||
if (isInPictureInPictureModeSafe()) {
|
||||
renderPiPMode(state)
|
||||
} else {
|
||||
renderFullScreenMode(state)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderFullScreenMode(state: VectorCallViewState) {
|
||||
views.callToolbar.isVisible = true
|
||||
views.callControlsView.isVisible = true
|
||||
views.callControlsView.updateForState(state)
|
||||
val callState = state.callState.invoke()
|
||||
views.callConnectingProgress.isVisible = false
|
||||
views.callActionText.setOnClickListener(null)
|
||||
views.callActionText.isVisible = false
|
||||
views.smallIsHeldIcon.isVisible = false
|
||||
when (callState) {
|
||||
is CallState.Idle,
|
||||
is CallState.CreateOffer,
|
||||
is CallState.Dialing -> {
|
||||
views.callVideoGroup.isInvisible = true
|
||||
is CallState.LocalRinging,
|
||||
is CallState.Dialing -> {
|
||||
views.fullscreenRenderer.isVisible = false
|
||||
views.pipRendererWrapper.isVisible = false
|
||||
views.callInfoGroup.isVisible = true
|
||||
views.callStatusText.setText(R.string.call_ring)
|
||||
views.callToolbar.setSubtitle(R.string.call_ringing)
|
||||
configureCallInfo(state)
|
||||
}
|
||||
|
||||
is CallState.LocalRinging -> {
|
||||
views.callVideoGroup.isInvisible = true
|
||||
is CallState.Answering -> {
|
||||
views.fullscreenRenderer.isVisible = false
|
||||
views.pipRendererWrapper.isVisible = false
|
||||
views.callInfoGroup.isVisible = true
|
||||
views.callStatusText.text = null
|
||||
views.callToolbar.setSubtitle(R.string.call_connecting)
|
||||
configureCallInfo(state)
|
||||
}
|
||||
|
||||
is CallState.Answering -> {
|
||||
views.callVideoGroup.isInvisible = true
|
||||
views.callInfoGroup.isVisible = true
|
||||
views.callStatusText.setText(R.string.call_connecting)
|
||||
views.callConnectingProgress.isVisible = true
|
||||
configureCallInfo(state)
|
||||
}
|
||||
is CallState.Connected -> {
|
||||
is CallState.Connected -> {
|
||||
views.callToolbar.subtitle = state.formattedDuration
|
||||
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
|
||||
if (state.isLocalOnHold || state.isRemoteOnHold) {
|
||||
views.smallIsHeldIcon.isVisible = true
|
||||
views.callVideoGroup.isInvisible = true
|
||||
views.fullscreenRenderer.isVisible = false
|
||||
views.pipRendererWrapper.isVisible = false
|
||||
views.callInfoGroup.isVisible = true
|
||||
configureCallInfo(state, blurAvatar = true)
|
||||
if (state.isRemoteOnHold) {
|
||||
views.callActionText.setText(R.string.call_resume_action)
|
||||
views.callActionText.isVisible = true
|
||||
views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.ToggleHoldResume) }
|
||||
views.callStatusText.setText(R.string.call_held_by_you)
|
||||
views.callToolbar.setSubtitle(R.string.call_held_by_you)
|
||||
} else {
|
||||
views.callActionText.isInvisible = true
|
||||
state.callInfo?.opponentUserItem?.let {
|
||||
views.callStatusText.text = getString(R.string.call_held_by_user, it.getBestName())
|
||||
views.callToolbar.subtitle = getString(R.string.call_held_by_user, it.getBestName())
|
||||
}
|
||||
}
|
||||
} else if (state.transferee !is VectorCallViewState.TransfereeState.NoTransferee) {
|
||||
@ -234,43 +305,90 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||
views.callActionText.text = getString(R.string.call_transfer_transfer_to_title, transfereeName)
|
||||
views.callActionText.isVisible = true
|
||||
views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.TransferCall) }
|
||||
views.callStatusText.text = state.formattedDuration
|
||||
configureCallInfo(state)
|
||||
} else {
|
||||
views.callStatusText.text = state.formattedDuration
|
||||
configureCallInfo(state)
|
||||
if (callArgs.isVideoCall) {
|
||||
views.callVideoGroup.isVisible = true
|
||||
if (state.isVideoCall) {
|
||||
views.fullscreenRenderer.isVisible = true
|
||||
views.pipRendererWrapper.isVisible = true
|
||||
views.callInfoGroup.isVisible = false
|
||||
views.pipRenderer.isVisible = !state.isVideoCaptureInError && state.otherKnownCallInfo == null
|
||||
} else {
|
||||
views.callVideoGroup.isInvisible = true
|
||||
views.fullscreenRenderer.isVisible = false
|
||||
views.pipRendererWrapper.isVisible = false
|
||||
views.callInfoGroup.isVisible = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This state is not final, if you change network, new candidates will be sent
|
||||
views.callVideoGroup.isInvisible = true
|
||||
views.fullscreenRenderer.isVisible = false
|
||||
views.pipRendererWrapper.isVisible = false
|
||||
views.callInfoGroup.isVisible = true
|
||||
configureCallInfo(state)
|
||||
views.callStatusText.setText(R.string.call_connecting)
|
||||
views.callConnectingProgress.isVisible = true
|
||||
views.callToolbar.setSubtitle(R.string.call_connecting)
|
||||
}
|
||||
}
|
||||
is CallState.Ended -> {
|
||||
views.callVideoGroup.isInvisible = true
|
||||
is CallState.Ended -> {
|
||||
views.fullscreenRenderer.isVisible = false
|
||||
views.pipRendererWrapper.isVisible = false
|
||||
views.callInfoGroup.isVisible = true
|
||||
views.callStatusText.setText(R.string.call_ended)
|
||||
views.callToolbar.setSubtitle(R.string.call_ended)
|
||||
configureCallInfo(state)
|
||||
}
|
||||
else -> {
|
||||
views.callVideoGroup.isInvisible = true
|
||||
else -> {
|
||||
views.fullscreenRenderer.isVisible = false
|
||||
views.pipRendererWrapper.isVisible = false
|
||||
views.callInfoGroup.isInvisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderPiPMode(state: VectorCallViewState) {
|
||||
val callState = state.callState.invoke()
|
||||
views.callToolbar.isVisible = false
|
||||
views.callControlsView.isVisible = false
|
||||
views.pipRendererWrapper.isVisible = false
|
||||
views.pipRenderer.isVisible = false
|
||||
views.callActionText.isVisible = false
|
||||
when (callState) {
|
||||
is CallState.Idle,
|
||||
is CallState.CreateOffer,
|
||||
is CallState.LocalRinging,
|
||||
is CallState.Dialing,
|
||||
is CallState.Answering -> {
|
||||
views.fullscreenRenderer.isVisible = false
|
||||
views.callInfoGroup.isVisible = false
|
||||
}
|
||||
is CallState.Connected -> {
|
||||
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
|
||||
if (state.isLocalOnHold || state.isRemoteOnHold) {
|
||||
views.smallIsHeldIcon.isVisible = true
|
||||
views.fullscreenRenderer.isVisible = false
|
||||
views.callInfoGroup.isVisible = true
|
||||
configureCallInfo(state, blurAvatar = true)
|
||||
} else {
|
||||
configureCallInfo(state)
|
||||
views.fullscreenRenderer.isVisible = true
|
||||
views.callInfoGroup.isVisible = false
|
||||
}
|
||||
} else {
|
||||
views.callInfoGroup.isVisible = false
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
views.fullscreenRenderer.isVisible = false
|
||||
views.callInfoGroup.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCallEnded(callState: CallState.Ended) {
|
||||
if (isInPictureInPictureModeSafe()) {
|
||||
val startIntent = Intent(this, VectorCallActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
|
||||
}
|
||||
startActivity(startIntent)
|
||||
}
|
||||
when (callState.reason) {
|
||||
EndCallReason.USER_BUSY -> {
|
||||
showEndCallDialog(R.string.call_ended_user_busy_title, R.string.call_ended_user_busy_description)
|
||||
@ -300,9 +418,14 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||
val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen_blur)
|
||||
avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter, addPlaceholder = false)
|
||||
if (state.transferee is VectorCallViewState.TransfereeState.NoTransferee) {
|
||||
views.participantNameText.text = it.getBestName()
|
||||
views.participantNameText.setTextOrHide(null)
|
||||
views.callToolbar.title = if (state.isVideoCall) {
|
||||
getString(R.string.video_call_with_participant, it.getBestName())
|
||||
} else {
|
||||
getString(R.string.audio_call_with_participant, it.getBestName())
|
||||
}
|
||||
} else {
|
||||
views.participantNameText.text = getString(R.string.call_transfer_consulting_with, it.getBestName())
|
||||
views.participantNameText.setTextOrHide(getString(R.string.call_transfer_consulting_with, it.getBestName()))
|
||||
}
|
||||
if (blurAvatar) {
|
||||
avatarRenderer.renderBlur(it, views.otherMemberAvatar, sampling = 2, rounded = true, colorFilter = colorFilter, addPlaceholder = true)
|
||||
@ -310,7 +433,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||
avatarRenderer.render(it, views.otherMemberAvatar)
|
||||
}
|
||||
}
|
||||
if (state.otherKnownCallInfo?.opponentUserItem == null) {
|
||||
if (state.otherKnownCallInfo?.opponentUserItem == null || isInPictureInPictureModeSafe()) {
|
||||
views.otherKnownCallLayout.isVisible = false
|
||||
} else {
|
||||
val otherCall = callManager.getCallById(state.otherKnownCallInfo.callId)
|
||||
@ -324,7 +447,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||
addPlaceholder = true
|
||||
)
|
||||
views.otherKnownCallLayout.isVisible = true
|
||||
views.otherSmallIsHeldIcon.isVisible = otherCall?.let { it.isLocalOnHold || it.remoteOnHold }.orFalse()
|
||||
views.otherSmallIsHeldIcon.isVisible = otherCall?.let { it.isLocalOnHold || it.isRemoteOnHold }.orFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@ -333,44 +456,60 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||
views.otherKnownCallLayout.setOnClickListener {
|
||||
withState(callViewModel) {
|
||||
val otherCall = callManager.getCallById(it.otherKnownCallInfo?.callId ?: "") ?: return@withState
|
||||
startActivity(newIntent(this, otherCall, null))
|
||||
finish()
|
||||
val callArgs = CallArgs(
|
||||
signalingRoomId = otherCall.nativeRoomId,
|
||||
callId = otherCall.callId,
|
||||
participantUserId = otherCall.mxCall.opponentUserId,
|
||||
isIncomingCall = !otherCall.mxCall.isOutgoing,
|
||||
isVideoCall = otherCall.mxCall.isVideoCall
|
||||
)
|
||||
callViewModel.handle(VectorCallViewActions.SwitchCall(callArgs))
|
||||
}
|
||||
}
|
||||
views.pipRendererWrapper.setOnClickListener {
|
||||
callViewModel.handle(VectorCallViewActions.ToggleCamera)
|
||||
}
|
||||
pipDraggrableView = views.pipRendererWrapper.setupDraggable()
|
||||
.setStickyMode(DraggableView.Mode.STICKY_XY)
|
||||
.build()
|
||||
|
||||
otherCallDraggableView = views.otherKnownCallLayout.setupDraggable()
|
||||
.setStickyMode(DraggableView.Mode.STICKY_XY)
|
||||
.build()
|
||||
}
|
||||
|
||||
private val permissionCameraLauncher = registerForPermissionsResult { allGranted, _ ->
|
||||
if (allGranted) {
|
||||
start()
|
||||
setupRenderersIfNeeded()
|
||||
} else {
|
||||
// TODO display something
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun start() {
|
||||
private fun setupRenderersIfNeeded() {
|
||||
detachRenderersIfNeeded()
|
||||
rootEglBase = EglUtils.rootEglBase ?: return Unit.also {
|
||||
Timber.tag(loggerTag.value).v("rootEglBase is null")
|
||||
finish()
|
||||
}
|
||||
|
||||
// Init Picture in Picture renderer
|
||||
views.pipRenderer.init(rootEglBase!!.eglBaseContext, null)
|
||||
views.pipRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
|
||||
|
||||
views.pipRenderer.apply {
|
||||
init(rootEglBase!!.eglBaseContext, null)
|
||||
setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_BALANCED)
|
||||
setEnableHardwareScaler(true)
|
||||
setZOrderMediaOverlay(true)
|
||||
}
|
||||
// Init Full Screen renderer
|
||||
views.fullscreenRenderer.init(rootEglBase!!.eglBaseContext, null)
|
||||
views.fullscreenRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL)
|
||||
|
||||
views.pipRenderer.setZOrderMediaOverlay(true)
|
||||
views.pipRenderer.setEnableHardwareScaler(true /* enabled */)
|
||||
views.fullscreenRenderer.setEnableHardwareScaler(true /* enabled */)
|
||||
|
||||
callManager.getCallById(callArgs.callId)?.attachViewRenderers(views.pipRenderer, views.fullscreenRenderer,
|
||||
intent.getStringExtra(EXTRA_MODE)?.takeIf { isFirstCreation() })
|
||||
|
||||
views.pipRenderer.setOnClickListener {
|
||||
callViewModel.handle(VectorCallViewActions.ToggleCamera)
|
||||
val callId = withState(callViewModel) { it.callId }
|
||||
callManager.getCallById(callId)?.also { webRtcCall ->
|
||||
webRtcCall.attachViewRenderers(views.pipRenderer, views.fullscreenRenderer, intent.getStringExtra(EXTRA_MODE))
|
||||
intent.removeExtra(EXTRA_MODE)
|
||||
}
|
||||
surfaceRenderersAreInitialized = true
|
||||
}
|
||||
@ -387,7 +526,8 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||
}.show(supportFragmentManager, FRAGMENT_DIAL_PAD_TAG)
|
||||
}
|
||||
is VectorCallViewEvents.ShowCallTransferScreen -> {
|
||||
navigator.openCallTransfer(this, callArgs.callId)
|
||||
val callId = withState(callViewModel) { it.callId }
|
||||
navigator.openCallTransfer(this, callId)
|
||||
}
|
||||
null -> {
|
||||
}
|
||||
@ -406,37 +546,8 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||
.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_MODE = "EXTRA_MODE"
|
||||
private const val FRAGMENT_DIAL_PAD_TAG = "FRAGMENT_DIAL_PAD_TAG"
|
||||
|
||||
const val OUTGOING_CREATED = "OUTGOING_CREATED"
|
||||
const val INCOMING_RINGING = "INCOMING_RINGING"
|
||||
const val INCOMING_ACCEPT = "INCOMING_ACCEPT"
|
||||
|
||||
fun newIntent(context: Context, call: WebRtcCall, mode: String?): Intent {
|
||||
return Intent(context, VectorCallActivity::class.java).apply {
|
||||
// what could be the best flags?
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
putExtra(MvRx.KEY_ARG, CallArgs(call.nativeRoomId, call.callId, call.mxCall.opponentUserId, !call.mxCall.isOutgoing, call.mxCall.isVideoCall))
|
||||
putExtra(EXTRA_MODE, mode)
|
||||
}
|
||||
}
|
||||
|
||||
fun newIntent(context: Context,
|
||||
callId: String,
|
||||
signalingRoomId: String,
|
||||
otherUserId: String,
|
||||
isIncomingCall: Boolean,
|
||||
isVideoCall: Boolean,
|
||||
mode: String?): Intent {
|
||||
return Intent(context, VectorCallActivity::class.java).apply {
|
||||
// what could be the best flags?
|
||||
flags = FLAG_ACTIVITY_CLEAR_TOP
|
||||
putExtra(MvRx.KEY_ARG, CallArgs(signalingRoomId, callId, otherUserId, isIncomingCall, isVideoCall))
|
||||
putExtra(EXTRA_MODE, mode)
|
||||
}
|
||||
}
|
||||
override fun didTapAudioSettings() {
|
||||
CallSoundDeviceChooserBottomSheet().show(supportFragmentManager, "SoundDeviceChooser")
|
||||
}
|
||||
|
||||
override fun didAcceptIncomingCall() {
|
||||
@ -459,8 +570,9 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||
callViewModel.handle(VectorCallViewActions.ToggleVideo)
|
||||
}
|
||||
|
||||
override fun returnToChat() {
|
||||
val args = RoomDetailArgs(callArgs.signalingRoomId)
|
||||
private fun returnToChat() {
|
||||
val roomId = withState(callViewModel) { it.roomId }
|
||||
val args = RoomDetailArgs(roomId)
|
||||
val intent = RoomDetailActivity.newIntent(this, args).apply {
|
||||
flags = FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
@ -508,4 +620,37 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_MODE = "EXTRA_MODE"
|
||||
private const val FRAGMENT_DIAL_PAD_TAG = "FRAGMENT_DIAL_PAD_TAG"
|
||||
|
||||
const val OUTGOING_CREATED = "OUTGOING_CREATED"
|
||||
const val INCOMING_RINGING = "INCOMING_RINGING"
|
||||
const val INCOMING_ACCEPT = "INCOMING_ACCEPT"
|
||||
|
||||
fun newIntent(context: Context, call: WebRtcCall, mode: String?): Intent {
|
||||
return Intent(context, VectorCallActivity::class.java).apply {
|
||||
// what could be the best flags?
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
putExtra(MvRx.KEY_ARG, CallArgs(call.nativeRoomId, call.callId, call.mxCall.opponentUserId, !call.mxCall.isOutgoing, call.mxCall.isVideoCall))
|
||||
putExtra(EXTRA_MODE, mode)
|
||||
}
|
||||
}
|
||||
|
||||
fun newIntent(context: Context,
|
||||
callId: String,
|
||||
signalingRoomId: String,
|
||||
otherUserId: String,
|
||||
isIncomingCall: Boolean,
|
||||
isVideoCall: Boolean,
|
||||
mode: String?): Intent {
|
||||
return Intent(context, VectorCallActivity::class.java).apply {
|
||||
// what could be the best flags?
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
putExtra(MvRx.KEY_ARG, CallArgs(signalingRoomId, callId, otherUserId, isIncomingCall, isVideoCall))
|
||||
putExtra(EXTRA_MODE, mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,8 @@ sealed class VectorCallViewActions : VectorViewModelAction {
|
||||
data class ChangeAudioDevice(val device: CallAudioManager.Device) : VectorCallViewActions()
|
||||
object OpenDialPad: VectorCallViewActions()
|
||||
data class SendDtmfDigit(val digit: String) : VectorCallViewActions()
|
||||
data class SwitchCall(val callArgs: CallArgs) : VectorCallViewActions()
|
||||
|
||||
object SwitchSoundDevice : VectorCallViewActions()
|
||||
object HeadSetButtonPressed : VectorCallViewActions()
|
||||
object ToggleCamera : VectorCallViewActions()
|
||||
|
@ -60,7 +60,7 @@ class VectorCallViewModel @AssistedInject constructor(
|
||||
setState {
|
||||
copy(
|
||||
isLocalOnHold = call?.isLocalOnHold ?: false,
|
||||
isRemoteOnHold = call?.remoteOnHold ?: false
|
||||
isRemoteOnHold = call?.isRemoteOnHold ?: false
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -144,7 +144,7 @@ class VectorCallViewModel @AssistedInject constructor(
|
||||
|
||||
override fun onAudioDevicesChange() {
|
||||
val currentSoundDevice = callManager.audioManager.selectedDevice ?: return
|
||||
if (currentSoundDevice == CallAudioManager.Device.PHONE) {
|
||||
if (currentSoundDevice == CallAudioManager.Device.Phone) {
|
||||
proximityManager.start()
|
||||
} else {
|
||||
proximityManager.stop()
|
||||
@ -172,7 +172,12 @@ class VectorCallViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
init {
|
||||
val webRtcCall = callManager.getCallById(initialState.callId)
|
||||
setupCallWithCurrentState()
|
||||
}
|
||||
|
||||
private fun setupCallWithCurrentState() = withState { state ->
|
||||
call?.removeListener(callListener)
|
||||
val webRtcCall = callManager.getCallById(state.callId)
|
||||
if (webRtcCall == null) {
|
||||
setState {
|
||||
copy(callState = Fail(IllegalArgumentException("No call")))
|
||||
@ -182,17 +187,19 @@ class VectorCallViewModel @AssistedInject constructor(
|
||||
callManager.addCurrentCallListener(currentCallListener)
|
||||
webRtcCall.addListener(callListener)
|
||||
val currentSoundDevice = callManager.audioManager.selectedDevice
|
||||
if (currentSoundDevice == CallAudioManager.Device.PHONE) {
|
||||
if (currentSoundDevice == CallAudioManager.Device.Phone) {
|
||||
proximityManager.start()
|
||||
}
|
||||
setState {
|
||||
copy(
|
||||
isAudioMuted = webRtcCall.micMuted,
|
||||
isVideoEnabled = !webRtcCall.videoMuted,
|
||||
isVideoCall = webRtcCall.mxCall.isVideoCall,
|
||||
callState = Success(webRtcCall.mxCall.state),
|
||||
callInfo = webRtcCall.extractCallInfo(),
|
||||
device = currentSoundDevice ?: CallAudioManager.Device.PHONE,
|
||||
device = currentSoundDevice ?: CallAudioManager.Device.Phone,
|
||||
isLocalOnHold = webRtcCall.isLocalOnHold,
|
||||
isRemoteOnHold = webRtcCall.remoteOnHold,
|
||||
isRemoteOnHold = webRtcCall.isRemoteOnHold,
|
||||
availableDevices = callManager.audioManager.availableDevices,
|
||||
isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT,
|
||||
canSwitchCamera = webRtcCall.canSwitchCamera(),
|
||||
@ -225,6 +232,7 @@ class VectorCallViewModel @AssistedInject constructor(
|
||||
override fun onCleared() {
|
||||
callManager.removeCurrentCallListener(currentCallListener)
|
||||
call?.removeListener(callListener)
|
||||
call = null
|
||||
proximityManager.stop()
|
||||
super.onCleared()
|
||||
}
|
||||
@ -302,9 +310,13 @@ class VectorCallViewModel @AssistedInject constructor(
|
||||
VectorCallViewEvents.ShowCallTransferScreen
|
||||
)
|
||||
}
|
||||
VectorCallViewActions.TransferCall -> {
|
||||
VectorCallViewActions.TransferCall -> {
|
||||
handleCallTransfer()
|
||||
}
|
||||
is VectorCallViewActions.SwitchCall -> {
|
||||
setState { VectorCallViewState(action.callArgs) }
|
||||
setupCallWithCurrentState()
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,7 @@ data class VectorCallViewState(
|
||||
val isHD: Boolean = false,
|
||||
val isFrontCamera: Boolean = true,
|
||||
val canSwitchCamera: Boolean = true,
|
||||
val device: CallAudioManager.Device = CallAudioManager.Device.PHONE,
|
||||
val device: CallAudioManager.Device = CallAudioManager.Device.Phone,
|
||||
val availableDevices: Set<CallAudioManager.Device> = emptySet(),
|
||||
val callState: Async<CallState> = Uninitialized,
|
||||
val otherKnownCallInfo: CallInfo? = null,
|
||||
|
@ -50,13 +50,17 @@ internal class API21AudioDeviceDetector(private val context: Context,
|
||||
|
||||
private fun getAvailableSoundDevices(): Set<CallAudioManager.Device> {
|
||||
return HashSet<CallAudioManager.Device>().apply {
|
||||
if (isBluetoothHeadsetOn()) add(CallAudioManager.Device.WIRELESS_HEADSET)
|
||||
if (isWiredHeadsetOn()) {
|
||||
add(CallAudioManager.Device.HEADSET)
|
||||
} else {
|
||||
add(CallAudioManager.Device.PHONE)
|
||||
if (isBluetoothHeadsetOn()) {
|
||||
connectedBlueToothHeadset?.connectedDevices?.forEach {
|
||||
add(CallAudioManager.Device.WirelessHeadset(it.name))
|
||||
}
|
||||
}
|
||||
add(CallAudioManager.Device.SPEAKER)
|
||||
if (isWiredHeadsetOn()) {
|
||||
add(CallAudioManager.Device.Headset)
|
||||
} else {
|
||||
add(CallAudioManager.Device.Phone)
|
||||
}
|
||||
add(CallAudioManager.Device.Speaker)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,10 +33,10 @@ internal class API23AudioDeviceDetector(private val audioManager: AudioManager,
|
||||
val deviceInfos = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
|
||||
for (info in deviceInfos) {
|
||||
when (info.type) {
|
||||
AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> devices.add(CallAudioManager.Device.WIRELESS_HEADSET)
|
||||
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> devices.add(CallAudioManager.Device.PHONE)
|
||||
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> devices.add(CallAudioManager.Device.SPEAKER)
|
||||
AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET, TYPE_USB_HEADSET -> devices.add(CallAudioManager.Device.HEADSET)
|
||||
AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> devices.add(CallAudioManager.Device.WirelessHeadset(info.productName.toString()))
|
||||
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> devices.add(CallAudioManager.Device.Phone)
|
||||
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> devices.add(CallAudioManager.Device.Speaker)
|
||||
AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET, TYPE_USB_HEADSET -> devices.add(CallAudioManager.Device.Headset)
|
||||
}
|
||||
}
|
||||
callAudioManager.replaceDevices(devices)
|
||||
|
@ -19,7 +19,10 @@ package im.vector.app.features.call.audio
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.getSystemService
|
||||
import im.vector.app.R
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import timber.log.Timber
|
||||
import java.util.HashSet
|
||||
@ -31,11 +34,11 @@ class CallAudioManager(private val context: Context, val configChange: (() -> Un
|
||||
private var audioDeviceDetector: AudioDeviceDetector? = null
|
||||
private var audioDeviceRouter: AudioDeviceRouter? = null
|
||||
|
||||
enum class Device {
|
||||
PHONE,
|
||||
SPEAKER,
|
||||
HEADSET,
|
||||
WIRELESS_HEADSET
|
||||
sealed class Device(@StringRes val titleRes: Int, @DrawableRes val drawableRes: Int) {
|
||||
object Phone : Device(R.string.sound_device_phone, R.drawable.ic_sound_device_phone)
|
||||
object Speaker : Device(R.string.sound_device_speaker, R.drawable.ic_sound_device_speaker)
|
||||
object Headset : Device(R.string.sound_device_headset, R.drawable.ic_sound_device_headphone)
|
||||
data class WirelessHeadset(val name: String?) : Device(R.string.sound_device_wireless_headset, R.drawable.ic_sound_device_wireless)
|
||||
}
|
||||
|
||||
enum class Mode {
|
||||
@ -133,19 +136,19 @@ class CallAudioManager(private val context: Context, val configChange: (() -> Un
|
||||
userSelectedDevice = null
|
||||
return true
|
||||
}
|
||||
val bluetoothAvailable = _availableDevices.contains(Device.WIRELESS_HEADSET)
|
||||
val headsetAvailable = _availableDevices.contains(Device.HEADSET)
|
||||
val availableBluetoothDevice = _availableDevices.firstOrNull { it is Device.WirelessHeadset }
|
||||
val headsetAvailable = _availableDevices.contains(Device.Headset)
|
||||
|
||||
// Pick the desired device based on what's available and the mode.
|
||||
var audioDevice: Device
|
||||
audioDevice = if (bluetoothAvailable) {
|
||||
Device.WIRELESS_HEADSET
|
||||
audioDevice = if (availableBluetoothDevice != null) {
|
||||
availableBluetoothDevice
|
||||
} else if (headsetAvailable) {
|
||||
Device.HEADSET
|
||||
Device.Headset
|
||||
} else if (mode == Mode.VIDEO_CALL) {
|
||||
Device.SPEAKER
|
||||
Device.Speaker
|
||||
} else {
|
||||
Device.PHONE
|
||||
Device.Phone
|
||||
}
|
||||
// Consider the user's selection
|
||||
if (userSelectedDevice != null && _availableDevices.contains(userSelectedDevice)) {
|
||||
|
@ -31,8 +31,8 @@ class DefaultAudioDeviceRouter(private val audioManager: AudioManager,
|
||||
private var focusRequestCompat: AudioFocusRequestCompat? = null
|
||||
|
||||
override fun setAudioRoute(device: CallAudioManager.Device) {
|
||||
audioManager.isSpeakerphoneOn = device === CallAudioManager.Device.SPEAKER
|
||||
setBluetoothAudioRoute(device === CallAudioManager.Device.WIRELESS_HEADSET)
|
||||
audioManager.isSpeakerphoneOn = device is CallAudioManager.Device.Speaker
|
||||
setBluetoothAudioRoute(device is CallAudioManager.Device.WirelessHeadset)
|
||||
}
|
||||
|
||||
override fun setMode(mode: CallAudioManager.Mode): Boolean {
|
||||
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.content.Context
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import org.jitsi.meet.sdk.BroadcastEvent
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class JitsiActiveConferenceHolder @Inject constructor(context: Context) {
|
||||
|
||||
private var activeConference: String? = null
|
||||
|
||||
init {
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(JitsiBroadcastEventObserver(context, this::onBroadcastEvent))
|
||||
}
|
||||
|
||||
fun isJoined(confId: String?): Boolean {
|
||||
return confId != null && activeConference?.endsWith(confId).orFalse()
|
||||
}
|
||||
|
||||
private fun onBroadcastEvent(broadcastEvent: BroadcastEvent) {
|
||||
when (broadcastEvent.type) {
|
||||
BroadcastEvent.Type.CONFERENCE_JOINED -> activeConference = broadcastEvent.extractConferenceUrl()
|
||||
BroadcastEvent.Type.CONFERENCE_TERMINATED -> activeConference = null
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import com.facebook.react.bridge.JavaOnlyMap
|
||||
import org.jitsi.meet.sdk.BroadcastEmitter
|
||||
import org.jitsi.meet.sdk.BroadcastEvent
|
||||
import org.jitsi.meet.sdk.JitsiMeet
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
|
||||
private const val CONFERENCE_URL_DATA_KEY = "url"
|
||||
|
||||
fun BroadcastEvent.extractConferenceUrl(): String? {
|
||||
return when (type) {
|
||||
BroadcastEvent.Type.CONFERENCE_TERMINATED,
|
||||
BroadcastEvent.Type.CONFERENCE_WILL_JOIN,
|
||||
BroadcastEvent.Type.CONFERENCE_JOINED -> data[CONFERENCE_URL_DATA_KEY] as? String
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
class JitsiBroadcastEmitter(private val context: Context) {
|
||||
|
||||
fun emitConferenceEnded() {
|
||||
val broadcastEventData = JavaOnlyMap.of(CONFERENCE_URL_DATA_KEY, JitsiMeet.getCurrentConference())
|
||||
BroadcastEmitter(context).sendBroadcast(BroadcastEvent.Type.CONFERENCE_TERMINATED.name, broadcastEventData)
|
||||
}
|
||||
}
|
||||
|
||||
class JitsiBroadcastEventObserver(private val context: Context,
|
||||
private val onBroadcastEvent: (BroadcastEvent) -> Unit) : LifecycleObserver {
|
||||
|
||||
// See https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-android-sdk#listening-for-broadcasted-events
|
||||
private val broadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
intent?.let { onBroadcastReceived(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
fun unregisterForBroadcastMessages() {
|
||||
tryOrNull("Unable to unregister receiver") {
|
||||
LocalBroadcastManager.getInstance(context).unregisterReceiver(broadcastReceiver)
|
||||
}
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||
fun registerForBroadcastMessages() {
|
||||
val intentFilter = IntentFilter()
|
||||
for (type in BroadcastEvent.Type.values()) {
|
||||
intentFilter.addAction(type.action)
|
||||
}
|
||||
tryOrNull("Unable to register receiver") {
|
||||
LocalBroadcastManager.getInstance(context).registerReceiver(broadcastReceiver, intentFilter)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBroadcastReceived(intent: Intent) {
|
||||
val event = BroadcastEvent(intent)
|
||||
onBroadcastEvent(event)
|
||||
}
|
||||
}
|
@ -108,8 +108,7 @@ class JitsiService @Inject constructor(
|
||||
this.avatar = userAvatar?.let { URL(it) }
|
||||
}
|
||||
val roomName = session.getRoomSummary(roomId)?.displayName
|
||||
val properties = session.widgetService().getWidgetComputedUrl(jitsiWidget, themeProvider.isLightTheme())
|
||||
?.let { url -> jitsiWidgetPropertiesFactory.create(url) } ?: throw IllegalStateException()
|
||||
val properties = extractProperties(jitsiWidget) ?: throw IllegalStateException()
|
||||
|
||||
val token = if (jitsiWidget.isOpenIdJWTAuthenticationRequired()) {
|
||||
getOpenIdJWTToken(roomId, properties.domain, userDisplayName ?: session.myUserId, userAvatar ?: "")
|
||||
@ -126,6 +125,11 @@ class JitsiService @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
fun extractProperties(jitsiWidget: Widget): JitsiWidgetProperties? {
|
||||
return session.widgetService().getWidgetComputedUrl(jitsiWidget, themeProvider.isLightTheme())
|
||||
?.let { url -> jitsiWidgetPropertiesFactory.create(url) }
|
||||
}
|
||||
|
||||
private fun Widget.isOpenIdJWTAuthenticationRequired(): Boolean {
|
||||
return widgetContent.data[JITSI_AUTH_KEY] == JITSI_OPEN_ID_TOKEN_JWT_AUTH
|
||||
}
|
||||
|
@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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 = translationX >= views.root.width / 4
|
||||
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.isWebRTCCallOptionAvailable()
|
||||
|| !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))
|
||||
}
|
||||
}
|
@ -16,18 +16,17 @@
|
||||
|
||||
package im.vector.app.features.call.conference
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.Success
|
||||
@ -41,6 +40,7 @@ import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.databinding.ActivityJitsiBinding
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.jitsi.meet.sdk.BroadcastEvent
|
||||
import org.jitsi.meet.sdk.JitsiMeet
|
||||
import org.jitsi.meet.sdk.JitsiMeetActivityDelegate
|
||||
import org.jitsi.meet.sdk.JitsiMeetActivityInterface
|
||||
import org.jitsi.meet.sdk.JitsiMeetConferenceOptions
|
||||
@ -71,13 +71,6 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
// See https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-android-sdk#listening-for-broadcasted-events
|
||||
private val broadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
intent?.let { onBroadcastReceived(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@ -94,8 +87,47 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
|
||||
JitsiCallViewEvents.LeaveConference -> handleLeaveConference()
|
||||
}.exhaustive
|
||||
}
|
||||
lifecycle.addObserver(JitsiBroadcastEventObserver(this, this::onBroadcastEvent))
|
||||
}
|
||||
|
||||
registerForBroadcastMessages()
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
JitsiMeetActivityDelegate.onHostResume(this)
|
||||
}
|
||||
|
||||
override fun initUiAndData() {
|
||||
super.initUiAndData()
|
||||
jitsiMeetView = JitsiMeetView(this)
|
||||
val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
|
||||
views.jitsiLayout.addView(jitsiMeetView, params)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
JitsiMeetActivityDelegate.onHostPause(this)
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
val currentConf = JitsiMeet.getCurrentConference()
|
||||
jitsiMeetView?.leave()
|
||||
jitsiMeetView?.dispose()
|
||||
// Fake emitting CONFERENCE_TERMINATED event when currentConf is not null (probably when closing the PiP screen).
|
||||
if (currentConf != null) {
|
||||
JitsiBroadcastEmitter(this).emitConferenceEnded()
|
||||
}
|
||||
JitsiMeetActivityDelegate.onHostDestroy(this)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
JitsiMeetActivityDelegate.onBackPressed()
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
|
||||
jitsiMeetView?.enterPictureInPicture()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLeaveConference() {
|
||||
@ -116,14 +148,16 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean,
|
||||
newConfig: Configuration) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||
checkIfActivityShouldBeFinished()
|
||||
Timber.w("onPictureInPictureModeChanged($isInPictureInPictureMode)")
|
||||
}
|
||||
|
||||
override fun initUiAndData() {
|
||||
super.initUiAndData()
|
||||
jitsiMeetView = JitsiMeetView(this)
|
||||
val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
|
||||
views.jitsiLayout.addView(jitsiMeetView, params)
|
||||
private fun checkIfActivityShouldBeFinished() {
|
||||
// OnStop is called when PiP mode is closed directly from the ui
|
||||
// If stopped is called and PiP mode is not active, we should finish the activity and remove the task as Android creates a new one for PiP.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) && !isInPictureInPictureMode) {
|
||||
finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderState(viewState: JitsiCallViewState) {
|
||||
@ -167,34 +201,6 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
|
||||
jitsiMeetView?.join(jitsiMeetConferenceOptions)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
JitsiMeetActivityDelegate.onHostPause(this)
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
JitsiMeetActivityDelegate.onHostResume(this)
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
JitsiMeetActivityDelegate.onBackPressed()
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
JitsiMeetActivityDelegate.onHostDestroy(this)
|
||||
unregisterForBroadcastMessages()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
|
||||
jitsiMeetView?.enterPictureInPicture()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
JitsiMeetActivityDelegate.onNewIntent(intent)
|
||||
|
||||
@ -217,24 +223,7 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
|
||||
JitsiMeetActivityDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
private fun registerForBroadcastMessages() {
|
||||
val intentFilter = IntentFilter()
|
||||
for (type in BroadcastEvent.Type.values()) {
|
||||
intentFilter.addAction(type.action)
|
||||
}
|
||||
tryOrNull("Unable to register receiver") {
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, intentFilter)
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregisterForBroadcastMessages() {
|
||||
tryOrNull("Unable to unregister receiver") {
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBroadcastReceived(intent: Intent) {
|
||||
val event = BroadcastEvent(intent)
|
||||
private fun onBroadcastEvent(event: BroadcastEvent) {
|
||||
Timber.v("Broadcast received: ${event.type}")
|
||||
when (event.type) {
|
||||
BroadcastEvent.Type.CONFERENCE_TERMINATED -> onConferenceTerminated(event.data)
|
||||
|
@ -175,7 +175,7 @@ class WebRtcCall(
|
||||
private set
|
||||
var videoMuted = false
|
||||
private set
|
||||
var remoteOnHold = false
|
||||
var isRemoteOnHold = false
|
||||
private set
|
||||
var isLocalOnHold = false
|
||||
private set
|
||||
@ -357,7 +357,7 @@ class WebRtcCall(
|
||||
|
||||
fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) {
|
||||
sessionScope?.launch(dispatcher) {
|
||||
Timber.tag(loggerTag.value).v("attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer")
|
||||
Timber.tag(loggerTag.value).v("attachViewRenderers localRenderer $localViewRenderer / $remoteViewRenderer")
|
||||
localSurfaceRenderers.addIfNeeded(localViewRenderer)
|
||||
remoteSurfaceRenderers.addIfNeeded(remoteViewRenderer)
|
||||
when (mode) {
|
||||
@ -614,12 +614,12 @@ class WebRtcCall(
|
||||
}
|
||||
|
||||
private fun updateMuteStatus() {
|
||||
val micShouldBeMuted = micMuted || remoteOnHold
|
||||
val micShouldBeMuted = micMuted || isRemoteOnHold
|
||||
localAudioTrack?.setEnabled(!micShouldBeMuted)
|
||||
remoteAudioTrack?.setEnabled(!remoteOnHold)
|
||||
val vidShouldBeMuted = videoMuted || remoteOnHold
|
||||
remoteAudioTrack?.setEnabled(!isRemoteOnHold)
|
||||
val vidShouldBeMuted = videoMuted || isRemoteOnHold
|
||||
localVideoTrack?.setEnabled(!vidShouldBeMuted)
|
||||
remoteVideoTrack?.setEnabled(!remoteOnHold)
|
||||
remoteVideoTrack?.setEnabled(!isRemoteOnHold)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -645,16 +645,16 @@ class WebRtcCall(
|
||||
|
||||
fun updateRemoteOnHold(onHold: Boolean) {
|
||||
sessionScope?.launch(dispatcher) {
|
||||
if (remoteOnHold == onHold) return@launch
|
||||
if (isRemoteOnHold == onHold) return@launch
|
||||
val direction: RtpTransceiver.RtpTransceiverDirection
|
||||
if (onHold) {
|
||||
wasLocalOnHold = isLocalOnHold
|
||||
remoteOnHold = true
|
||||
isRemoteOnHold = true
|
||||
isLocalOnHold = true
|
||||
direction = RtpTransceiver.RtpTransceiverDirection.SEND_ONLY
|
||||
timer.pause()
|
||||
} else {
|
||||
remoteOnHold = false
|
||||
isRemoteOnHold = false
|
||||
isLocalOnHold = wasLocalOnHold
|
||||
onCallBecomeActive(this@WebRtcCall)
|
||||
direction = RtpTransceiver.RtpTransceiverDirection.SEND_RECV
|
||||
|
@ -16,17 +16,20 @@
|
||||
|
||||
package im.vector.app.features.call.webrtc
|
||||
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
|
||||
fun WebRtcCall.getOpponentAsMatrixItem(session: Session): MatrixItem? {
|
||||
return session.getRoomSummary(nativeRoomId)?.let { roomSummary ->
|
||||
return session.getRoom(nativeRoomId)?.let { room ->
|
||||
val roomSummary = room.roomSummary() ?: return@let null
|
||||
// Fallback to RoomSummary if there is no other member.
|
||||
if (roomSummary.otherMemberIds.isEmpty()) {
|
||||
if (roomSummary.otherMemberIds.isEmpty().orFalse()) {
|
||||
roomSummary.toMatrixItem()
|
||||
} else {
|
||||
roomSummary.otherMemberIds.first().let { session.getUser(it)?.toMatrixItem() }
|
||||
val userId = roomSummary.otherMemberIds.first()
|
||||
return room.getRoomMember(userId)?.toMatrixItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -147,10 +147,6 @@ class RoomDevToolActivity : SimpleFragmentActivity(), RoomDevToolViewModel.Facto
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
if (item.itemId == R.id.menuItemEdit) {
|
||||
viewModel.handle(RoomDevToolAction.MenuEdit)
|
||||
return true
|
||||
|
@ -22,7 +22,6 @@ import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.get
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.iterator
|
||||
import androidx.fragment.app.Fragment
|
||||
@ -39,8 +38,8 @@ import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.ui.views.CurrentCallsView
|
||||
import im.vector.app.core.ui.views.CurrentCallsViewPresenter
|
||||
import im.vector.app.core.ui.views.KeysBackupBanner
|
||||
import im.vector.app.core.ui.views.KnownCallsViewHolder
|
||||
import im.vector.app.databinding.FragmentHomeDetailBinding
|
||||
import im.vector.app.features.call.SharedKnownCallsViewModel
|
||||
import im.vector.app.features.call.VectorCallActivity
|
||||
@ -117,7 +116,7 @@ class HomeDetailFragment @Inject constructor(
|
||||
return FragmentHomeDetailBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
private val activeCallViewHolder = KnownCallsViewHolder()
|
||||
private val currentCallsViewPresenter = CurrentCallsViewPresenter()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
@ -190,11 +189,16 @@ class HomeDetailFragment @Inject constructor(
|
||||
sharedCallActionViewModel
|
||||
.liveKnownCalls
|
||||
.observe(viewLifecycleOwner, {
|
||||
activeCallViewHolder.updateCall(callManager.getCurrentCall(), callManager.getCalls())
|
||||
currentCallsViewPresenter.updateCall(callManager.getCurrentCall(), callManager.getCalls())
|
||||
invalidateOptionsMenu()
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
currentCallsViewPresenter.unBind()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// update notification tab if needed
|
||||
@ -291,12 +295,7 @@ class HomeDetailFragment @Inject constructor(
|
||||
}
|
||||
|
||||
private fun setupActiveCallView() {
|
||||
activeCallViewHolder.bind(
|
||||
views.activeCallPiP,
|
||||
views.activeCallView,
|
||||
views.activeCallPiPWrap,
|
||||
this
|
||||
)
|
||||
currentCallsViewPresenter.bind(views.currentCallsView, this)
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
|
@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
import org.jitsi.meet.sdk.BroadcastEvent
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
||||
@ -89,9 +90,14 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
||||
object ManageIntegrations : RoomDetailAction()
|
||||
data class AddJitsiWidget(val withVideo: Boolean) : RoomDetailAction()
|
||||
data class RemoveWidget(val widgetId: String) : RoomDetailAction()
|
||||
|
||||
object JoinJitsiCall: RoomDetailAction()
|
||||
object LeaveJitsiCall: RoomDetailAction()
|
||||
|
||||
data class EnsureNativeWidgetAllowed(val widget: Widget,
|
||||
val userJustAccepted: Boolean,
|
||||
val grantedEvents: RoomDetailViewEvents) : RoomDetailAction()
|
||||
data class UpdateJoinJitsiCallStatus(val jitsiEvent: BroadcastEvent): RoomDetailAction()
|
||||
|
||||
data class OpenOrCreateDm(val userId: String) : RoomDetailAction()
|
||||
data class JumpToReadReceipt(val userId: String) : RoomDetailAction()
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.app.features.home.room.detail
|
||||
|
||||
import android.animation.ArgbEvaluator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
@ -65,6 +66,7 @@ import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.args
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.jakewharton.rxbinding3.view.focusChanges
|
||||
import com.jakewharton.rxbinding3.widget.textChanges
|
||||
@ -88,10 +90,9 @@ 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.CurrentCallsView
|
||||
import im.vector.app.core.ui.views.CurrentCallsViewPresenter
|
||||
import im.vector.app.core.ui.views.FailedMessagesWarningView
|
||||
import im.vector.app.core.ui.views.KnownCallsViewHolder
|
||||
import im.vector.app.core.ui.views.NotificationAreaView
|
||||
import im.vector.app.core.utils.Debouncer
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
@ -123,6 +124,8 @@ import im.vector.app.features.attachments.preview.AttachmentsPreviewArgs
|
||||
import im.vector.app.features.attachments.toGroupedContentAttachmentData
|
||||
import im.vector.app.features.call.SharedKnownCallsViewModel
|
||||
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.webrtc.WebRtcCallManager
|
||||
import im.vector.app.features.command.Command
|
||||
@ -182,6 +185,7 @@ 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.BroadcastEvent
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
@ -213,6 +217,7 @@ import java.net.URL
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import android.animation.ValueAnimator
|
||||
|
||||
@Parcelize
|
||||
data class RoomDetailArgs(
|
||||
@ -307,7 +312,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
|
||||
|
||||
private var lockSendButton = false
|
||||
private val knownCallsViewHolder = KnownCallsViewHolder()
|
||||
private val currentCallsViewPresenter = CurrentCallsViewPresenter()
|
||||
|
||||
private lateinit var emojiPopup: EmojiPopup
|
||||
|
||||
@ -321,6 +326,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
lifecycle.addObserver(JitsiBroadcastEventObserver(vectorBaseActivity, this::onBroadcastJitsiEvent))
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
|
||||
knownCallsViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java)
|
||||
@ -344,9 +350,9 @@ class RoomDetailFragment @Inject constructor(
|
||||
setupJumpToReadMarkerView()
|
||||
setupActiveCallView()
|
||||
setupJumpToBottomView()
|
||||
setupConfBannerView()
|
||||
setupEmojiPopup()
|
||||
setupFailedMessagesWarningView()
|
||||
setupRemoveJitsiWidgetView()
|
||||
setupVoiceMessageView()
|
||||
|
||||
views.roomToolbarContentView.debouncedClicks {
|
||||
@ -363,7 +369,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
knownCallsViewModel
|
||||
.liveKnownCalls
|
||||
.observe(viewLifecycleOwner, {
|
||||
knownCallsViewHolder.updateCall(callManager.getCurrentCall(), it)
|
||||
currentCallsViewPresenter.updateCall(callManager.getCurrentCall(), it)
|
||||
invalidateOptionsMenu()
|
||||
})
|
||||
|
||||
@ -412,6 +418,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.ShowWaitingView -> vectorBaseActivity.showWaitingView()
|
||||
RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView()
|
||||
is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it)
|
||||
@ -436,6 +443,26 @@ 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()
|
||||
}
|
||||
|
||||
private fun onBroadcastJitsiEvent(jitsiEvent: BroadcastEvent) {
|
||||
roomDetailViewModel.handle(RoomDetailAction.UpdateJoinJitsiCallStatus(jitsiEvent))
|
||||
}
|
||||
|
||||
private fun onCannotRecord() {
|
||||
// Update the UI, cancel the animation
|
||||
views.voiceMessageRecorderView.initVoiceRecordingViews()
|
||||
@ -559,31 +586,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
|
||||
@ -769,7 +771,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
override fun onDestroyView() {
|
||||
timelineEventController.callback = null
|
||||
timelineEventController.removeModelBuildListener(modelBuildListener)
|
||||
views.activeCallView.callback = null
|
||||
currentCallsViewPresenter.unBind()
|
||||
modelBuildListener = null
|
||||
autoCompleter.clear()
|
||||
debouncer.cancelAll()
|
||||
@ -780,7 +782,6 @@ class RoomDetailFragment @Inject constructor(
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
knownCallsViewHolder.unBind()
|
||||
roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
|
||||
super.onDestroy()
|
||||
}
|
||||
@ -816,12 +817,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
}
|
||||
|
||||
private fun setupActiveCallView() {
|
||||
knownCallsViewHolder.bind(
|
||||
views.activeCallPiP,
|
||||
views.activeCallView,
|
||||
views.activeCallPiPWrap,
|
||||
this
|
||||
)
|
||||
currentCallsViewPresenter.bind(views.currentCallsView, this)
|
||||
}
|
||||
|
||||
private fun navigateToEvent(action: RoomDetailViewEvents.NavigateToEvent) {
|
||||
@ -872,6 +868,22 @@ class RoomDetailFragment @Inject constructor(
|
||||
onOptionsItemSelected(menuItem)
|
||||
}
|
||||
}
|
||||
val joinConfItem = menu.findItem(R.id.join_conference)
|
||||
joinConfItem.actionView.findViewById<MaterialButton>(R.id.join_conference_button).also { joinButton ->
|
||||
joinButton.setOnClickListener { roomDetailViewModel.handle(RoomDetailAction.JoinJitsiCall) }
|
||||
val colorFrom = ContextCompat.getColor(joinButton.context, R.color.palette_element_green)
|
||||
val colorTo = ContextCompat.getColor(joinButton.context, R.color.join_conference_animated_color)
|
||||
// Animate button color to highlight
|
||||
ValueAnimator.ofObject(ArgbEvaluator(), colorFrom, colorTo).apply {
|
||||
repeatMode = ValueAnimator.REVERSE
|
||||
repeatCount = ValueAnimator.INFINITE
|
||||
duration = 500
|
||||
addUpdateListener { animator ->
|
||||
val color = animator.animatedValue as Int
|
||||
joinButton.setBackgroundColor(color)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
@ -880,7 +892,8 @@ class RoomDetailFragment @Inject constructor(
|
||||
}
|
||||
withState(roomDetailViewModel) { state ->
|
||||
// Set the visual state of the call buttons (voice/video) to enabled/disabled according to user permissions
|
||||
val callButtonsEnabled = when (state.asyncRoomSummary.invoke()?.joinedMembersCount) {
|
||||
val hasCallInRoom = callManager.getCallsByRoomId(state.roomId).isNotEmpty() || state.jitsiState.hasJoined
|
||||
val callButtonsEnabled = !hasCallInRoom && when (state.asyncRoomSummary.invoke()?.joinedMembersCount) {
|
||||
1 -> false
|
||||
2 -> state.isAllowedToStartWebRTCCall
|
||||
else -> state.isAllowedToManageWidgets
|
||||
@ -891,14 +904,8 @@ class RoomDetailFragment @Inject constructor(
|
||||
|
||||
val matrixAppsMenuItem = menu.findItem(R.id.open_matrix_apps)
|
||||
val widgetsCount = state.activeRoomWidgets.invoke()?.size ?: 0
|
||||
if (widgetsCount > 0) {
|
||||
val actionView = matrixAppsMenuItem.actionView
|
||||
actionView
|
||||
.findViewById<ImageView>(R.id.action_view_icon_image)
|
||||
.setColorFilter(colorProvider.getColorFromAttribute(R.attr.colorPrimary))
|
||||
actionView.findViewById<TextView>(R.id.cart_badge).setTextOrHide("$widgetsCount")
|
||||
matrixAppsMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
||||
} else {
|
||||
val hasOnlyJitsiWidget = widgetsCount == 1 && state.hasActiveJitsiWidget()
|
||||
if (widgetsCount == 0 || hasOnlyJitsiWidget) {
|
||||
// icon should be default color no badge
|
||||
val actionView = matrixAppsMenuItem.actionView
|
||||
actionView
|
||||
@ -906,6 +913,13 @@ class RoomDetailFragment @Inject constructor(
|
||||
.setColorFilter(ThemeUtils.getColor(requireContext(), R.attr.vctr_content_secondary))
|
||||
actionView.findViewById<TextView>(R.id.cart_badge).isVisible = false
|
||||
matrixAppsMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
|
||||
} else {
|
||||
val actionView = matrixAppsMenuItem.actionView
|
||||
actionView
|
||||
.findViewById<ImageView>(R.id.action_view_icon_image)
|
||||
.setColorFilter(colorProvider.getColorFromAttribute(R.attr.colorPrimary))
|
||||
actionView.findViewById<TextView>(R.id.cart_badge).setTextOrHide("$widgetsCount")
|
||||
matrixAppsMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -932,10 +946,6 @@ class RoomDetailFragment @Inject constructor(
|
||||
callActionsHandler.onVideoCallClicked()
|
||||
true
|
||||
}
|
||||
R.id.hangup_call -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.EndCall)
|
||||
true
|
||||
}
|
||||
R.id.search -> {
|
||||
handleSearchAction()
|
||||
true
|
||||
@ -1362,7 +1372,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) {
|
||||
|
@ -45,6 +45,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
|
||||
|
||||
data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents()
|
||||
data class JoinJitsiConference(val widget: Widget, val withVideo: Boolean) : RoomDetailViewEvents()
|
||||
object LeaveJitsiConference : RoomDetailViewEvents()
|
||||
|
||||
object OpenInvitePeople : RoomDetailViewEvents()
|
||||
object OpenSetRoomAvatarDialog : RoomDetailViewEvents()
|
||||
|
@ -38,6 +38,7 @@ import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.mvrx.runCatchingToAsync
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.call.conference.JitsiActiveConferenceHolder
|
||||
import im.vector.app.features.attachments.toContentAttachmentData
|
||||
import im.vector.app.features.call.conference.JitsiService
|
||||
import im.vector.app.features.call.lookup.CallProtocolsChecker
|
||||
@ -51,7 +52,6 @@ import im.vector.app.features.home.room.detail.composer.VoiceMessageHelper
|
||||
import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
|
||||
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
|
||||
import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||
import im.vector.app.features.home.room.typing.TypingHelper
|
||||
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
|
||||
@ -66,6 +66,7 @@ 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.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.MatrixPatterns
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
@ -115,12 +116,12 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
private val session: Session,
|
||||
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider,
|
||||
private val stickerPickerActionHandler: StickerPickerActionHandler,
|
||||
private val roomSummariesHolder: RoomSummariesHolder,
|
||||
private val typingHelper: TypingHelper,
|
||||
private val callManager: WebRtcCallManager,
|
||||
private val chatEffectManager: ChatEffectManager,
|
||||
private val directRoomHelper: DirectRoomHelper,
|
||||
private val jitsiService: JitsiService,
|
||||
private val activeConferenceHolder: JitsiActiveConferenceHolder,
|
||||
private val voiceMessageHelper: VoiceMessageHelper,
|
||||
private val voicePlayerHelper: VoicePlayerHelper,
|
||||
timelineFactory: TimelineFactory
|
||||
@ -241,9 +242,25 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
.map { widgets ->
|
||||
widgets.filter { it.isActive }
|
||||
}
|
||||
.execute {
|
||||
copy(activeRoomWidgets = it)
|
||||
.execute { widgets ->
|
||||
copy(activeRoomWidgets = widgets)
|
||||
}
|
||||
|
||||
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,
|
||||
hasJoined = activeConferenceHolder.isJoined(jitsiConfId)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeMyRoomMember() {
|
||||
@ -308,6 +325,9 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
is RoomDetailAction.EndCall -> handleEndCall()
|
||||
is RoomDetailAction.ManageIntegrations -> handleManageIntegrations()
|
||||
is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action)
|
||||
is RoomDetailAction.UpdateJoinJitsiCallStatus -> handleJitsiCallJoinStatus(action)
|
||||
is RoomDetailAction.JoinJitsiCall -> handleJoinJitsiCall()
|
||||
is RoomDetailAction.LeaveJitsiCall -> handleLeaveJitsiCall()
|
||||
is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId)
|
||||
is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action)
|
||||
is RoomDetailAction.CancelSend -> handleCancel(action)
|
||||
@ -340,6 +360,33 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun handleJitsiCallJoinStatus(action: RoomDetailAction.UpdateJoinJitsiCallStatus) = withState { state ->
|
||||
if (state.jitsiState.confId == null) {
|
||||
// If jitsi widget is removed while on the call
|
||||
if (state.jitsiState.hasJoined) {
|
||||
setState { copy(jitsiState = jitsiState.copy(hasJoined = false)) }
|
||||
}
|
||||
return@withState
|
||||
}
|
||||
when (action.jitsiEvent.type) {
|
||||
BroadcastEvent.Type.CONFERENCE_JOINED,
|
||||
BroadcastEvent.Type.CONFERENCE_TERMINATED -> {
|
||||
setState { copy(jitsiState = jitsiState.copy(hasJoined = activeConferenceHolder.isJoined(jitsiState.confId))) }
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLeaveJitsiCall() {
|
||||
_viewEvents.post(RoomDetailViewEvents.LeaveJitsiConference)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
private fun handleAcceptCall(action: RoomDetailAction.AcceptCall) {
|
||||
callManager.getCallById(action.callId)?.also {
|
||||
_viewEvents.post(RoomDetailViewEvents.DisplayAndAcceptCall(it))
|
||||
@ -448,10 +495,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 {
|
||||
@ -467,7 +519,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -682,9 +738,10 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
R.id.timeline_setting -> true
|
||||
R.id.invite -> state.canInvite
|
||||
R.id.open_matrix_apps -> true
|
||||
R.id.voice_call,
|
||||
R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty()
|
||||
R.id.hangup_call -> callManager.getCallsByRoomId(state.roomId).isNotEmpty()
|
||||
R.id.voice_call -> state.isWebRTCCallOptionAvailable()
|
||||
R.id.video_call -> state.isWebRTCCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined
|
||||
// Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^
|
||||
R.id.join_conference -> !state.isWebRTCCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined
|
||||
R.id.search -> true
|
||||
R.id.dev_tools -> vectorPreferences.developerMode()
|
||||
else -> false
|
||||
@ -1515,7 +1572,6 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
|
||||
private fun observeSummaryState() {
|
||||
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
|
||||
roomSummariesHolder.set(summary)
|
||||
setState {
|
||||
val typingMessage = typingHelper.getTypingMessage(summary.typingUsers)
|
||||
copy(
|
||||
@ -1563,7 +1619,6 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
roomSummariesHolder.remove(room.roomId)
|
||||
timeline.dispose()
|
||||
timeline.removeAllListeners()
|
||||
if (vectorPreferences.sendTypingNotifs()) {
|
||||
|
@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
@ -26,6 +27,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||
|
||||
/**
|
||||
* Describes the current send mode:
|
||||
@ -55,6 +57,14 @@ sealed class UnreadState {
|
||||
data class HasUnread(val firstUnreadEventId: String) : UnreadState()
|
||||
}
|
||||
|
||||
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 deleteWidgetInProgress: Boolean = false
|
||||
)
|
||||
|
||||
data class RoomDetailViewState(
|
||||
val roomId: String,
|
||||
val eventId: String?,
|
||||
@ -75,7 +85,8 @@ data class RoomDetailViewState(
|
||||
val canInvite: Boolean = true,
|
||||
val isAllowedToManageWidgets: Boolean = false,
|
||||
val isAllowedToStartWebRTCCall: Boolean = true,
|
||||
val hasFailedSending: Boolean = false
|
||||
val hasFailedSending: Boolean = false,
|
||||
val jitsiState: JitsiState = JitsiState()
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: RoomDetailArgs) : this(
|
||||
@ -85,5 +96,11 @@ data class RoomDetailViewState(
|
||||
highlightedEventId = args.eventId
|
||||
)
|
||||
|
||||
fun isWebRTCCallOptionAvailable() = (asyncRoomSummary.invoke()?.joinedMembersCount ?: 0) <= 2
|
||||
|
||||
// This checks directly on the active room widgets.
|
||||
// It can differs for a short period of time on the JitsiState as its computed async.
|
||||
fun hasActiveJitsiWidget() = activeRoomWidgets()?.any { it.type == WidgetType.Jitsi && it.isActive }.orFalse()
|
||||
|
||||
fun isDm() = asyncRoomSummary()?.isDirect == true
|
||||
}
|
||||
|
@ -26,7 +26,6 @@ import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL
|
||||
import im.vector.app.core.utils.checkPermissions
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||
|
||||
class StartCallActionsHandler(
|
||||
private val roomId: String,
|
||||
@ -36,7 +35,7 @@ class StartCallActionsHandler(
|
||||
private val roomDetailViewModel: RoomDetailViewModel,
|
||||
private val startCallActivityResultLauncher: ActivityResultLauncher<Array<String>>,
|
||||
private val showDialogWithMessage: (String) -> Unit,
|
||||
private val onTapToReturnToCall: () -> Unit) {
|
||||
private val onTapToReturnToCall: () -> Unit) {
|
||||
|
||||
fun onVideoCallClicked() {
|
||||
handleCallRequest(true)
|
||||
@ -61,16 +60,8 @@ class StartCallActionsHandler(
|
||||
}
|
||||
2 -> {
|
||||
val currentCall = callManager.getCurrentCall()
|
||||
if (currentCall != null) {
|
||||
// resume existing if same room, if not prompt to kill and then restart new call?
|
||||
if (currentCall.signalingRoomId == roomId) {
|
||||
onTapToReturnToCall()
|
||||
}
|
||||
// else {
|
||||
// TODO might not work well, and should prompt
|
||||
// webRtcPeerConnectionManager.endCall()
|
||||
// safeStartCall(it, isVideoCall)
|
||||
// }
|
||||
if (currentCall?.signalingRoomId == roomId) {
|
||||
onTapToReturnToCall()
|
||||
} else if (!state.isAllowedToStartWebRTCCall) {
|
||||
showDialogWithMessage(fragment.getString(
|
||||
if (state.isDm()) {
|
||||
@ -96,9 +87,8 @@ class StartCallActionsHandler(
|
||||
}
|
||||
))
|
||||
} else {
|
||||
if (state.activeRoomWidgets()?.filter { it.type == WidgetType.Jitsi }?.any() == true) {
|
||||
// A conference is already in progress!
|
||||
showDialogWithMessage(fragment.getString(R.string.conference_call_in_progress))
|
||||
if (state.hasActiveJitsiWidget()) {
|
||||
// A conference is already in progress, return
|
||||
} else {
|
||||
MaterialAlertDialogBuilder(fragment.requireContext())
|
||||
.setTitle(if (isVideoCall) R.string.video_meeting else R.string.audio_meeting)
|
||||
|
@ -31,8 +31,7 @@ import im.vector.app.core.epoxy.LoadingItem_
|
||||
import im.vector.app.core.extensions.localDateTime
|
||||
import im.vector.app.core.extensions.nextOrNull
|
||||
import im.vector.app.core.extensions.prevOrNull
|
||||
import im.vector.app.core.resources.UserPreferencesProvider
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import im.vector.app.features.home.room.detail.JitsiState
|
||||
import im.vector.app.features.home.room.detail.RoomDetailAction
|
||||
import im.vector.app.features.home.room.detail.RoomDetailViewState
|
||||
import im.vector.app.features.home.room.detail.UnreadState
|
||||
@ -40,6 +39,7 @@ import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItem
|
||||
import im.vector.app.features.home.room.detail.timeline.factory.ReadReceiptsItemFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroups
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineControllerInterceptorHelper
|
||||
@ -47,14 +47,13 @@ import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiff
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
|
||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import im.vector.app.features.media.VideoContentRenderer
|
||||
@ -65,6 +64,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
||||
@ -80,14 +80,30 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
|
||||
private val mergedHeaderItemFactory: MergedHeaderItemFactory,
|
||||
private val session: Session,
|
||||
private val callManager: WebRtcCallManager,
|
||||
@TimelineEventControllerHandler
|
||||
private val backgroundHandler: Handler,
|
||||
private val userPreferencesProvider: UserPreferencesProvider,
|
||||
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper,
|
||||
private val readReceiptsItemFactory: ReadReceiptsItemFactory
|
||||
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
|
||||
|
||||
/**
|
||||
* This is a partial state of the RoomDetailViewState
|
||||
*/
|
||||
data class PartialState(
|
||||
val unreadState: UnreadState = UnreadState.Unknown,
|
||||
val highlightedEventId: String? = null,
|
||||
val jitsiState: JitsiState = JitsiState(),
|
||||
val roomSummary: RoomSummary? = null
|
||||
) {
|
||||
|
||||
constructor(state: RoomDetailViewState) : this(
|
||||
unreadState = state.unreadState,
|
||||
highlightedEventId = state.highlightedEventId,
|
||||
jitsiState = state.jitsiState,
|
||||
roomSummary = state.asyncRoomSummary()
|
||||
)
|
||||
}
|
||||
|
||||
interface Callback :
|
||||
BaseCallback,
|
||||
ReactionPillCallback,
|
||||
@ -149,14 +165,15 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
|
||||
// Map eventId to adapter position
|
||||
private val adapterPositionMapping = HashMap<String, Int>()
|
||||
private val timelineEventsGroups = TimelineEventsGroups()
|
||||
private val receiptsByEvent = HashMap<String, MutableList<ReadReceipt>>()
|
||||
private val modelCache = arrayListOf<CacheItemData?>()
|
||||
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
||||
private var inSubmitList: Boolean = false
|
||||
private var hasReachedInvite: Boolean = false
|
||||
private var hasUTD: Boolean = false
|
||||
private var unreadState: UnreadState = UnreadState.Unknown
|
||||
private var positionOfReadMarker: Int? = null
|
||||
private var eventIdToHighlight: String? = null
|
||||
private var partialState: PartialState = PartialState()
|
||||
|
||||
var callback: Callback? = null
|
||||
var timeline: Timeline? = null
|
||||
@ -174,7 +191,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
// it's sent by the same user so we are sure we have up to date information.
|
||||
val invalidatedSenderId: String? = currentSnapshot.getOrNull(position)?.senderInfo?.userId
|
||||
val prevDisplayableEventIndex = currentSnapshot.subList(0, position).indexOfLast {
|
||||
timelineEventVisibilityHelper.shouldShowEvent(it, eventIdToHighlight)
|
||||
timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId)
|
||||
}
|
||||
if (prevDisplayableEventIndex != -1 && currentSnapshot[prevDisplayableEventIndex].senderInfo.userId == invalidatedSenderId) {
|
||||
modelCache[prevDisplayableEventIndex] = null
|
||||
@ -215,9 +232,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
|
||||
private val interceptorHelper = TimelineControllerInterceptorHelper(
|
||||
::positionOfReadMarker,
|
||||
adapterPositionMapping,
|
||||
userPreferencesProvider,
|
||||
callManager
|
||||
adapterPositionMapping
|
||||
)
|
||||
|
||||
init {
|
||||
@ -226,29 +241,22 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
}
|
||||
|
||||
override fun intercept(models: MutableList<EpoxyModel<*>>) = synchronized(modelCache) {
|
||||
interceptorHelper.intercept(models, unreadState, timeline, callback)
|
||||
interceptorHelper.intercept(models, partialState.unreadState, timeline, callback)
|
||||
}
|
||||
|
||||
fun update(viewState: RoomDetailViewState) {
|
||||
var requestModelBuild = false
|
||||
if (eventIdToHighlight != viewState.highlightedEventId) {
|
||||
fun update(viewState: RoomDetailViewState) = synchronized(modelCache) {
|
||||
val newPartialState = PartialState(viewState)
|
||||
if (partialState.highlightedEventId != newPartialState.highlightedEventId) {
|
||||
// Clear cache to force a refresh
|
||||
synchronized(modelCache) {
|
||||
for (i in 0 until modelCache.size) {
|
||||
if (modelCache[i]?.eventId == viewState.highlightedEventId
|
||||
|| modelCache[i]?.eventId == eventIdToHighlight) {
|
||||
modelCache[i] = null
|
||||
}
|
||||
for (i in 0 until modelCache.size) {
|
||||
if (modelCache[i]?.eventId == viewState.highlightedEventId
|
||||
|| modelCache[i]?.eventId == partialState.highlightedEventId) {
|
||||
modelCache[i] = null
|
||||
}
|
||||
}
|
||||
eventIdToHighlight = viewState.highlightedEventId
|
||||
requestModelBuild = true
|
||||
}
|
||||
if (this.unreadState != viewState.unreadState) {
|
||||
this.unreadState = viewState.unreadState
|
||||
requestModelBuild = true
|
||||
}
|
||||
if (requestModelBuild) {
|
||||
if (newPartialState != partialState) {
|
||||
partialState = newPartialState
|
||||
requestModelBuild()
|
||||
}
|
||||
}
|
||||
@ -346,31 +354,33 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
if (modelCache.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val receiptsByEvents = getReadReceiptsByShownEvent()
|
||||
val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(receiptsByEvents)
|
||||
preprocessReverseEvents()
|
||||
val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(receiptsByEvent)
|
||||
(0 until modelCache.size).forEach { position ->
|
||||
val event = currentSnapshot[position]
|
||||
val nextEvent = currentSnapshot.nextOrNull(position)
|
||||
val prevEvent = currentSnapshot.prevOrNull(position)
|
||||
val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull {
|
||||
timelineEventVisibilityHelper.shouldShowEvent(it, eventIdToHighlight)
|
||||
timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId)
|
||||
}
|
||||
val params = TimelineItemFactoryParams(
|
||||
event = event,
|
||||
prevEvent = prevEvent,
|
||||
nextEvent = nextEvent,
|
||||
nextDisplayableEvent = nextDisplayableEvent,
|
||||
highlightedEventId = eventIdToHighlight,
|
||||
lastSentEventIdWithoutReadReceipts = lastSentEventWithoutReadReceipts,
|
||||
callback = callback
|
||||
)
|
||||
// Should be build if not cached or if model should be refreshed
|
||||
if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild == true) {
|
||||
if (modelCache[position] == null || modelCache[position]?.isCacheable == false) {
|
||||
val timelineEventsGroup = timelineEventsGroups.getOrNull(event)
|
||||
val params = TimelineItemFactoryParams(
|
||||
event = event,
|
||||
prevEvent = prevEvent,
|
||||
nextEvent = nextEvent,
|
||||
nextDisplayableEvent = nextDisplayableEvent,
|
||||
partialState = partialState,
|
||||
lastSentEventIdWithoutReadReceipts = lastSentEventWithoutReadReceipts,
|
||||
callback = callback,
|
||||
eventsGroup = timelineEventsGroup
|
||||
)
|
||||
modelCache[position] = buildCacheItem(params)
|
||||
}
|
||||
val itemCachedData = modelCache[position] ?: return@forEach
|
||||
// Then update with additional models if needed
|
||||
modelCache[position] = itemCachedData.enrichWithModels(event, nextEvent, position, receiptsByEvents)
|
||||
modelCache[position] = itemCachedData.enrichWithModels(event, nextEvent, position, receiptsByEvent)
|
||||
}
|
||||
}
|
||||
|
||||
@ -384,12 +394,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
it.id(event.localId)
|
||||
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
||||
}
|
||||
val shouldTriggerBuild = eventModel is AbsMessageItem && eventModel.attributes.informationData.sendStateDecoration == SendStateDecoration.SENT
|
||||
val isCacheable = eventModel is ItemWithEvents && eventModel.isCacheable()
|
||||
return CacheItemData(
|
||||
localId = event.localId,
|
||||
eventId = event.root.eventId,
|
||||
eventModel = eventModel,
|
||||
shouldTriggerBuild = shouldTriggerBuild)
|
||||
isCacheable = isCacheable
|
||||
)
|
||||
}
|
||||
|
||||
private fun CacheItemData.enrichWithModels(event: TimelineEvent,
|
||||
@ -399,10 +410,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
val wantsDateSeparator = wantsDateSeparator(event, nextEvent)
|
||||
val mergedHeaderModel = mergedHeaderItemFactory.create(event,
|
||||
nextEvent = nextEvent,
|
||||
partialState = partialState,
|
||||
items = this@TimelineEventController.currentSnapshot,
|
||||
addDaySeparator = wantsDateSeparator,
|
||||
currentPosition = position,
|
||||
eventIdToHighlight = eventIdToHighlight,
|
||||
eventIdToHighlight = partialState.highlightedEventId,
|
||||
callback = callback
|
||||
) {
|
||||
requestModelBuild()
|
||||
@ -431,7 +443,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
return null
|
||||
}
|
||||
// If the event is not shown, we go to the next one
|
||||
if (!timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) {
|
||||
if (!timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId)) {
|
||||
continue
|
||||
}
|
||||
// If the event is sent by us, we update the holder with the eventId and stop the search
|
||||
@ -442,19 +454,18 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getReadReceiptsByShownEvent(): Map<String, List<ReadReceipt>> {
|
||||
val receiptsByEvent = HashMap<String, MutableList<ReadReceipt>>()
|
||||
if (!userPreferencesProvider.shouldShowReadReceipts()) {
|
||||
return receiptsByEvent
|
||||
}
|
||||
var lastShownEventId: String? = null
|
||||
private fun preprocessReverseEvents() {
|
||||
receiptsByEvent.clear()
|
||||
timelineEventsGroups.clear()
|
||||
val itr = currentSnapshot.listIterator(currentSnapshot.size)
|
||||
var lastShownEventId: String? = null
|
||||
while (itr.hasPrevious()) {
|
||||
val event = itr.previous()
|
||||
timelineEventsGroups.addOrIgnore(event)
|
||||
val currentReadReceipts = ArrayList(event.readReceipts).filter {
|
||||
it.user.userId != session.myUserId
|
||||
}
|
||||
if (timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) {
|
||||
if (timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId)) {
|
||||
lastShownEventId = event.eventId
|
||||
}
|
||||
if (lastShownEventId == null) {
|
||||
@ -463,7 +474,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
val existingReceipts = receiptsByEvent.getOrPut(lastShownEventId) { ArrayList() }
|
||||
existingReceipts.addAll(currentReadReceipts)
|
||||
}
|
||||
return receiptsByEvent
|
||||
}
|
||||
|
||||
private fun buildDaySeparatorItem(originServerTs: Long?): DaySeparatorItem {
|
||||
@ -536,6 +546,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
val eventModel: EpoxyModel<*>? = null,
|
||||
val mergedHeaderModel: BasedMergedItem<*>? = null,
|
||||
val formattedDayModel: DaySeparatorItem? = null,
|
||||
val shouldTriggerBuild: Boolean = false
|
||||
val isCacheable: Boolean = true
|
||||
)
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ import im.vector.app.features.html.VectorHtmlCompressor
|
||||
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
|
||||
import im.vector.app.features.reactions.data.EmojiDataSource
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
@ -207,7 +208,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||
EventType.CALL_CANDIDATES,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_ANSWER -> {
|
||||
noticeEventFormatter.format(timelineEvent)
|
||||
noticeEventFormatter.format(timelineEvent, room?.roomSummary()?.isDirect.orFalse())
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
@ -16,127 +16,122 @@
|
||||
package im.vector.app.features.home.room.detail.timeline.factory
|
||||
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.features.call.vectorCallService
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import im.vector.app.core.resources.UserPreferencesProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.CallSignalingEventsGroup
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class CallItemFactory @Inject constructor(
|
||||
private val session: Session,
|
||||
private val userPreferencesProvider: UserPreferencesProvider,
|
||||
private val messageColorProvider: MessageColorProvider,
|
||||
private val messageInformationDataFactory: MessageInformationDataFactory,
|
||||
private val messageItemAttributesFactory: MessageItemAttributesFactory,
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val roomSummariesHolder: RoomSummariesHolder,
|
||||
private val callManager: WebRtcCallManager
|
||||
) {
|
||||
private val noticeItemFactory: NoticeItemFactory) {
|
||||
|
||||
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
|
||||
val event = params.event
|
||||
if (event.root.eventId == null) return null
|
||||
val roomId = event.roomId
|
||||
val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents()
|
||||
val callEventGrouper = params.eventsGroup?.let { CallSignalingEventsGroup(it) } ?: return null
|
||||
val roomSummary = params.partialState.roomSummary ?: return null
|
||||
val informationData = messageInformationDataFactory.create(params)
|
||||
val callSignalingContent = event.getCallSignalingContent() ?: return null
|
||||
val callId = callSignalingContent.callId ?: return null
|
||||
val call = callManager.getCallById(callId)
|
||||
val callKind = when {
|
||||
call == null -> CallTileTimelineItem.CallKind.UNKNOWN
|
||||
call.mxCall.isVideoCall -> CallTileTimelineItem.CallKind.VIDEO
|
||||
else -> CallTileTimelineItem.CallKind.AUDIO
|
||||
}
|
||||
return when (event.root.getClearType()) {
|
||||
val callKind = if (callEventGrouper.isVideo()) CallTileTimelineItem.CallKind.VIDEO else CallTileTimelineItem.CallKind.AUDIO
|
||||
val callItem = when (event.root.getClearType()) {
|
||||
EventType.CALL_ANSWER -> {
|
||||
createCallTileTimelineItem(
|
||||
roomId = roomId,
|
||||
callId = callId,
|
||||
callStatus = CallTileTimelineItem.CallStatus.IN_CALL,
|
||||
callKind = callKind,
|
||||
callback = params.callback,
|
||||
highlight = params.isHighlighted,
|
||||
informationData = informationData,
|
||||
isStillActive = call != null
|
||||
)
|
||||
if (callEventGrouper.isInCall()) {
|
||||
createCallTileTimelineItem(
|
||||
roomSummary = roomSummary,
|
||||
callId = callEventGrouper.callId,
|
||||
callStatus = CallTileTimelineItem.CallStatus.IN_CALL,
|
||||
callKind = callKind,
|
||||
callback = params.callback,
|
||||
highlight = params.isHighlighted,
|
||||
informationData = informationData,
|
||||
isStillActive = callEventGrouper.isInCall(),
|
||||
formattedDuration = callEventGrouper.formattedDuration()
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
EventType.CALL_INVITE -> {
|
||||
createCallTileTimelineItem(
|
||||
roomId = roomId,
|
||||
callId = callId,
|
||||
callStatus = CallTileTimelineItem.CallStatus.INVITED,
|
||||
callKind = callKind,
|
||||
callback = params.callback,
|
||||
highlight = params.isHighlighted,
|
||||
informationData = informationData,
|
||||
isStillActive = call != null
|
||||
)
|
||||
if (callEventGrouper.isRinging()) {
|
||||
createCallTileTimelineItem(
|
||||
roomSummary = roomSummary,
|
||||
callId = callEventGrouper.callId,
|
||||
callStatus = CallTileTimelineItem.CallStatus.INVITED,
|
||||
callKind = callKind,
|
||||
callback = params.callback,
|
||||
highlight = params.isHighlighted,
|
||||
informationData = informationData,
|
||||
isStillActive = callEventGrouper.isRinging(),
|
||||
formattedDuration = callEventGrouper.formattedDuration()
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
EventType.CALL_REJECT -> {
|
||||
createCallTileTimelineItem(
|
||||
roomId = roomId,
|
||||
callId = callId,
|
||||
roomSummary = roomSummary,
|
||||
callId = callEventGrouper.callId,
|
||||
callStatus = CallTileTimelineItem.CallStatus.REJECTED,
|
||||
callKind = callKind,
|
||||
callback = params.callback,
|
||||
highlight = params.isHighlighted,
|
||||
informationData = informationData,
|
||||
isStillActive = false
|
||||
isStillActive = false,
|
||||
formattedDuration = callEventGrouper.formattedDuration()
|
||||
)
|
||||
}
|
||||
EventType.CALL_HANGUP -> {
|
||||
createCallTileTimelineItem(
|
||||
roomId = roomId,
|
||||
callId = callId,
|
||||
callStatus = CallTileTimelineItem.CallStatus.ENDED,
|
||||
roomSummary = roomSummary,
|
||||
callId = callEventGrouper.callId,
|
||||
callStatus = if (callEventGrouper.callWasMissed()) CallTileTimelineItem.CallStatus.MISSED else CallTileTimelineItem.CallStatus.ENDED,
|
||||
callKind = callKind,
|
||||
callback = params.callback,
|
||||
highlight = params.isHighlighted,
|
||||
informationData = informationData,
|
||||
isStillActive = false
|
||||
isStillActive = false,
|
||||
formattedDuration = callEventGrouper.formattedDuration()
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun TimelineEvent.getCallSignalingContent(): CallSignalingContent? {
|
||||
return when (root.getClearType()) {
|
||||
EventType.CALL_INVITE -> root.getClearContent().toModel<CallInviteContent>()
|
||||
EventType.CALL_HANGUP -> root.getClearContent().toModel<CallHangupContent>()
|
||||
EventType.CALL_REJECT -> root.getClearContent().toModel<CallRejectContent>()
|
||||
EventType.CALL_ANSWER -> root.getClearContent().toModel<CallAnswerContent>()
|
||||
else -> null
|
||||
return if (callItem == null && showHiddenEvents) {
|
||||
// Fallback to notice item for showing hidden events
|
||||
noticeItemFactory.create(params)
|
||||
} else {
|
||||
callItem
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCallTileTimelineItem(
|
||||
roomId: String,
|
||||
roomSummary: RoomSummary,
|
||||
callId: String,
|
||||
callKind: CallTileTimelineItem.CallKind,
|
||||
callStatus: CallTileTimelineItem.CallStatus,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
isStillActive: Boolean,
|
||||
formattedDuration: String,
|
||||
callback: TimelineEventController.Callback?
|
||||
): CallTileTimelineItem? {
|
||||
val correctedRoomId = session.vectorCallService.userMapper.nativeRoomForVirtualRoom(roomId) ?: roomId
|
||||
val userOfInterest = roomSummariesHolder.get(correctedRoomId)?.toMatrixItem() ?: return null
|
||||
val userOfInterest = roomSummary.toMatrixItem()
|
||||
val attributes = messageItemAttributesFactory.create(null, informationData, callback).let {
|
||||
CallTileTimelineItem.Attributes(
|
||||
callId = callId,
|
||||
@ -144,6 +139,7 @@ class CallItemFactory @Inject constructor(
|
||||
callStatus = callStatus,
|
||||
informationData = informationData,
|
||||
avatarRenderer = it.avatarRenderer,
|
||||
formattedDuration = formattedDuration,
|
||||
messageColorProvider = messageColorProvider,
|
||||
itemClickListener = it.itemClickListener,
|
||||
itemLongClickListener = it.itemLongClickListener,
|
||||
|
@ -22,7 +22,6 @@ import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration
|
||||
@ -47,8 +46,7 @@ import javax.inject.Inject
|
||||
class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val roomSummariesHolder: RoomSummariesHolder,
|
||||
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
|
||||
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
|
||||
|
||||
private val collapsedEventIds = linkedSetOf<Long>()
|
||||
private val mergeItemCollapseStates = HashMap<Long, Boolean>()
|
||||
@ -60,6 +58,7 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
|
||||
fun create(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
items: List<TimelineEvent>,
|
||||
partialState: TimelineEventController.PartialState,
|
||||
addDaySeparator: Boolean,
|
||||
currentPosition: Int,
|
||||
eventIdToHighlight: String?,
|
||||
@ -70,18 +69,17 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
|
||||
&& event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel<RoomCreateContent>()?.creator)) {
|
||||
// It's the first item before room.create
|
||||
// Collapse all room configuration events
|
||||
buildRoomCreationMergedSummary(currentPosition, items, event, eventIdToHighlight, requestModelBuild, callback)
|
||||
buildRoomCreationMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback)
|
||||
} else if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) {
|
||||
null
|
||||
} else {
|
||||
buildMembershipEventsMergedSummary(currentPosition, items, event, eventIdToHighlight, requestModelBuild, callback)
|
||||
buildMembershipEventsMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isDirectRoom(roomId: String) = roomSummariesHolder.get(roomId)?.isDirect.orFalse()
|
||||
|
||||
private fun buildMembershipEventsMergedSummary(currentPosition: Int,
|
||||
items: List<TimelineEvent>,
|
||||
partialState: TimelineEventController.PartialState,
|
||||
event: TimelineEvent,
|
||||
eventIdToHighlight: String?,
|
||||
requestModelBuild: () -> Unit,
|
||||
@ -102,7 +100,7 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
|
||||
memberName = mergedEvent.senderInfo.disambiguatedDisplayName,
|
||||
localId = mergedEvent.localId,
|
||||
eventId = mergedEvent.root.eventId ?: "",
|
||||
isDirectRoom = isDirectRoom(event.roomId)
|
||||
isDirectRoom = partialState.isDirectRoom()
|
||||
)
|
||||
mergedData.add(data)
|
||||
}
|
||||
@ -141,6 +139,7 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
|
||||
|
||||
private fun buildRoomCreationMergedSummary(currentPosition: Int,
|
||||
items: List<TimelineEvent>,
|
||||
partialState: TimelineEventController.PartialState,
|
||||
event: TimelineEvent,
|
||||
eventIdToHighlight: String?,
|
||||
requestModelBuild: () -> Unit,
|
||||
@ -173,7 +172,7 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
|
||||
memberName = mergedEvent.senderInfo.disambiguatedDisplayName,
|
||||
localId = mergedEvent.localId,
|
||||
eventId = mergedEvent.root.eventId ?: "",
|
||||
isDirectRoom = isDirectRoom(event.roomId)
|
||||
isDirectRoom = partialState.isDirectRoom()
|
||||
)
|
||||
mergedData.add(data)
|
||||
}
|
||||
@ -206,7 +205,7 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
|
||||
isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
callback = callback,
|
||||
currentUserId = currentUserId,
|
||||
roomSummary = roomSummariesHolder.get(event.roomId),
|
||||
roomSummary = partialState.roomSummary,
|
||||
canChangeAvatar = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_AVATAR) ?: false,
|
||||
canChangeTopic = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_TOPIC) ?: false,
|
||||
canChangeName = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_NAME) ?: false
|
||||
@ -223,6 +222,10 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
|
||||
} else null
|
||||
}
|
||||
|
||||
private fun TimelineEventController.PartialState.isDirectRoom(): Boolean {
|
||||
return roomSummary?.isDirect.orFalse()
|
||||
}
|
||||
|
||||
fun isCollapsed(localId: Long): Boolean {
|
||||
return collapsedEventIds.contains(localId)
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvide
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.item.NoticeItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.NoticeItem_
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import javax.inject.Inject
|
||||
|
||||
class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEventFormatter,
|
||||
@ -31,7 +32,7 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv
|
||||
|
||||
fun create(params: TimelineItemFactoryParams): NoticeItem? {
|
||||
val event = params.event
|
||||
val formattedText = eventFormatter.format(event) ?: return null
|
||||
val formattedText = eventFormatter.format(event, isDm = params.partialState.roomSummary?.isDirect.orFalse()) ?: return null
|
||||
val informationData = informationDataFactory.create(params)
|
||||
val attributes = NoticeItem.Attributes(
|
||||
avatarRenderer = avatarRenderer,
|
||||
|
@ -17,6 +17,7 @@
|
||||
package im.vector.app.features.home.room.detail.timeline.factory
|
||||
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroup
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
|
||||
data class TimelineItemFactoryParams(
|
||||
@ -24,9 +25,14 @@ data class TimelineItemFactoryParams(
|
||||
val prevEvent: TimelineEvent? = null,
|
||||
val nextEvent: TimelineEvent? = null,
|
||||
val nextDisplayableEvent: TimelineEvent? = null,
|
||||
val highlightedEventId: String? = null,
|
||||
val partialState: TimelineEventController.PartialState = TimelineEventController.PartialState(),
|
||||
val lastSentEventIdWithoutReadReceipts: String? = null,
|
||||
val callback: TimelineEventController.Callback? = null
|
||||
val callback: TimelineEventController.Callback? = null,
|
||||
val eventsGroup: TimelineEventsGroup? = null
|
||||
) {
|
||||
|
||||
val highlightedEventId: String?
|
||||
get() = partialState.highlightedEventId
|
||||
|
||||
val isHighlighted = highlightedEventId == event.eventId
|
||||
}
|
||||
|
@ -16,34 +16,28 @@
|
||||
|
||||
package im.vector.app.features.home.room.detail.timeline.factory
|
||||
|
||||
import im.vector.app.ActiveSessionDataSource
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.resources.UserPreferencesProvider
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.JitsiWidgetEventsGroup
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.item.WidgetTileTimelineItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.WidgetTileTimelineItem_
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem_
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetContent
|
||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class WidgetItemFactory @Inject constructor(
|
||||
private val sp: StringProvider,
|
||||
private val messageItemAttributesFactory: MessageItemAttributesFactory,
|
||||
private val informationDataFactory: MessageInformationDataFactory,
|
||||
private val noticeItemFactory: NoticeItemFactory,
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val activeSessionDataSource: ActiveSessionDataSource
|
||||
) {
|
||||
private val currentUserId: String?
|
||||
get() = activeSessionDataSource.currentValue?.orNull()?.myUserId
|
||||
|
||||
private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId
|
||||
private val messageColorProvider: MessageColorProvider,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val userPreferencesProvider: UserPreferencesProvider) {
|
||||
|
||||
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
|
||||
val event = params.event
|
||||
@ -51,62 +45,54 @@ class WidgetItemFactory @Inject constructor(
|
||||
val previousWidgetContent: WidgetContent? = event.root.resolvedPrevContent().toModel()
|
||||
|
||||
return when (WidgetType.fromString(widgetContent.type ?: previousWidgetContent?.type ?: "")) {
|
||||
WidgetType.Jitsi -> createJitsiItem(params, widgetContent, previousWidgetContent)
|
||||
WidgetType.Jitsi -> createJitsiItem(params, widgetContent)
|
||||
// There is lot of other widget types we could improve here
|
||||
else -> noticeItemFactory.create(params)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createJitsiItem(params: TimelineItemFactoryParams,
|
||||
widgetContent: WidgetContent,
|
||||
previousWidgetContent: WidgetContent?): VectorEpoxyModel<*> {
|
||||
val timelineEvent = params.event
|
||||
private fun createJitsiItem(params: TimelineItemFactoryParams, widgetContent: WidgetContent): VectorEpoxyModel<*>? {
|
||||
val informationData = informationDataFactory.create(params)
|
||||
val attributes = messageItemAttributesFactory.create(null, informationData, params.callback)
|
||||
|
||||
val disambiguatedDisplayName = timelineEvent.senderInfo.disambiguatedDisplayName
|
||||
val message = if (widgetContent.isActive()) {
|
||||
val widgetName = widgetContent.getHumanName()
|
||||
if (previousWidgetContent?.isActive().orFalse()) {
|
||||
// Widget has been modified
|
||||
if (timelineEvent.root.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.notice_widget_jitsi_modified_by_you, widgetName)
|
||||
} else {
|
||||
sp.getString(R.string.notice_widget_jitsi_modified, disambiguatedDisplayName, widgetName)
|
||||
}
|
||||
val userOfInterest = params.partialState.roomSummary?.toMatrixItem() ?: return null
|
||||
val isActiveTile = widgetContent.isActive()
|
||||
val jitsiWidgetEventsGroup = params.eventsGroup?.let { JitsiWidgetEventsGroup(it) } ?: return null
|
||||
val isCallStillActive = jitsiWidgetEventsGroup.isStillActive()
|
||||
val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents()
|
||||
if (isActiveTile && !isCallStillActive) {
|
||||
return if (showHiddenEvents) {
|
||||
noticeItemFactory.create(params)
|
||||
} else {
|
||||
// Widget has been added
|
||||
if (timelineEvent.root.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.notice_widget_jitsi_added_by_you, widgetName)
|
||||
} else {
|
||||
sp.getString(R.string.notice_widget_jitsi_added, disambiguatedDisplayName, widgetName)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Widget has been removed
|
||||
val widgetName = previousWidgetContent?.getHumanName()
|
||||
if (timelineEvent.root.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.notice_widget_jitsi_removed_by_you, widgetName)
|
||||
} else {
|
||||
sp.getString(R.string.notice_widget_jitsi_removed, disambiguatedDisplayName, widgetName)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
return WidgetTileTimelineItem_()
|
||||
.attributes(
|
||||
WidgetTileTimelineItem.Attributes(
|
||||
title = message,
|
||||
drawableStart = R.drawable.ic_video,
|
||||
informationData = informationData,
|
||||
avatarRenderer = attributes.avatarRenderer,
|
||||
messageColorProvider = attributes.messageColorProvider,
|
||||
itemLongClickListener = attributes.itemLongClickListener,
|
||||
itemClickListener = attributes.itemClickListener,
|
||||
reactionPillCallback = attributes.reactionPillCallback,
|
||||
readReceiptsCallback = attributes.readReceiptsCallback,
|
||||
emojiTypeFace = attributes.emojiTypeFace
|
||||
)
|
||||
)
|
||||
val callStatus = if (isActiveTile && params.event.root.stateKey == params.partialState.jitsiState.widgetId) {
|
||||
if (params.partialState.jitsiState.hasJoined) {
|
||||
CallTileTimelineItem.CallStatus.IN_CALL
|
||||
} else {
|
||||
CallTileTimelineItem.CallStatus.INVITED
|
||||
}
|
||||
} else {
|
||||
CallTileTimelineItem.CallStatus.ENDED
|
||||
}
|
||||
val attributes = CallTileTimelineItem.Attributes(
|
||||
callId = jitsiWidgetEventsGroup.callId,
|
||||
callKind = CallTileTimelineItem.CallKind.CONFERENCE,
|
||||
callStatus = callStatus,
|
||||
informationData = informationData,
|
||||
avatarRenderer = avatarRenderer,
|
||||
messageColorProvider = messageColorProvider,
|
||||
itemClickListener = null,
|
||||
itemLongClickListener = null,
|
||||
reactionPillCallback = params.callback,
|
||||
readReceiptsCallback = params.callback,
|
||||
userOfInterest = userOfInterest,
|
||||
callback = params.callback,
|
||||
isStillActive = isCallStillActive,
|
||||
formattedDuration = ""
|
||||
)
|
||||
return CallTileTimelineItem_()
|
||||
.attributes(attributes)
|
||||
.highlighted(params.isHighlighted)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ class DisplayableEventFormatter @Inject constructor(
|
||||
private val noticeEventFormatter: NoticeEventFormatter
|
||||
) {
|
||||
|
||||
fun format(timelineEvent: TimelineEvent, appendAuthor: Boolean): CharSequence {
|
||||
fun format(timelineEvent: TimelineEvent, isDm: Boolean, appendAuthor: Boolean): CharSequence {
|
||||
if (timelineEvent.root.isRedacted()) {
|
||||
return noticeEventFormatter.formatRedactedEvent(timelineEvent.root)
|
||||
}
|
||||
@ -135,7 +135,7 @@ class DisplayableEventFormatter @Inject constructor(
|
||||
}
|
||||
else -> {
|
||||
return span {
|
||||
text = noticeEventFormatter.format(timelineEvent) ?: ""
|
||||
text = noticeEventFormatter.format(timelineEvent, isDm) ?: ""
|
||||
textStyle = "italic"
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,6 @@ package im.vector.app.features.home.room.detail.timeline.format
|
||||
import im.vector.app.ActiveSessionDataSource
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
|
||||
import im.vector.app.features.roomprofile.permissions.RoleFormatter
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import org.matrix.android.sdk.api.extensions.appendNl
|
||||
@ -40,7 +39,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomNameContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomServerAclContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
||||
@ -58,7 +56,6 @@ class NoticeEventFormatter @Inject constructor(
|
||||
private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
|
||||
private val roleFormatter: RoleFormatter,
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val roomSummariesHolder: RoomSummariesHolder,
|
||||
private val sp: StringProvider
|
||||
) {
|
||||
|
||||
@ -67,28 +64,25 @@ class NoticeEventFormatter @Inject constructor(
|
||||
|
||||
private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId
|
||||
|
||||
private fun RoomSummary?.isDm() = this?.isDirect.orFalse()
|
||||
|
||||
fun format(timelineEvent: TimelineEvent): CharSequence? {
|
||||
val rs = roomSummariesHolder.get(timelineEvent.roomId)
|
||||
fun format(timelineEvent: TimelineEvent, isDm: Boolean): CharSequence? {
|
||||
return when (val type = timelineEvent.root.getClearType()) {
|
||||
EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
|
||||
EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root, rs)
|
||||
EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm)
|
||||
EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root, isDm)
|
||||
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
|
||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
|
||||
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm)
|
||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm)
|
||||
EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_HISTORY_VISIBILITY ->
|
||||
formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
|
||||
formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm)
|
||||
EventType.STATE_ROOM_SERVER_ACL -> formatRoomServerAclEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
|
||||
EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm)
|
||||
EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_WIDGET,
|
||||
EventType.STATE_ROOM_WIDGET_LEGACY -> formatWidgetEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
|
||||
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm)
|
||||
EventType.STATE_ROOM_POWER_LEVELS -> formatRoomPowerLevels(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_CANDIDATES,
|
||||
@ -176,20 +170,20 @@ class NoticeEventFormatter @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun format(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? {
|
||||
fun format(event: Event, senderName: String?, isDm: Boolean): CharSequence? {
|
||||
return when (val type = event.getClearType()) {
|
||||
EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName, rs)
|
||||
EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName, isDm)
|
||||
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName)
|
||||
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName)
|
||||
EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(event, senderName)
|
||||
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName, rs)
|
||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(event, senderName, rs)
|
||||
EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName, rs)
|
||||
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName, isDm)
|
||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(event, senderName, isDm)
|
||||
EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName, isDm)
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_REJECT,
|
||||
EventType.CALL_ANSWER -> formatCallEvent(type, event, senderName)
|
||||
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName, rs)
|
||||
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName, isDm)
|
||||
else -> {
|
||||
Timber.v("Type $type not handled by this formatter")
|
||||
null
|
||||
@ -201,14 +195,14 @@ class NoticeEventFormatter @Inject constructor(
|
||||
return "Debug: event type \"${event.getClearType()}\""
|
||||
}
|
||||
|
||||
private fun formatRoomCreateEvent(event: Event, rs: RoomSummary?): CharSequence? {
|
||||
private fun formatRoomCreateEvent(event: Event, isDm: Boolean): CharSequence? {
|
||||
return event.getClearContent().toModel<RoomCreateContent>()
|
||||
?.takeIf { it.creator.isNullOrBlank().not() }
|
||||
?.let {
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_created_by_you else R.string.notice_room_created_by_you)
|
||||
sp.getString(if (isDm) R.string.notice_direct_room_created_by_you else R.string.notice_room_created_by_you)
|
||||
} else {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_created else R.string.notice_room_created, it.creator)
|
||||
sp.getString(if (isDm) R.string.notice_direct_room_created else R.string.notice_room_created, it.creator)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -230,11 +224,11 @@ class NoticeEventFormatter @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatRoomTombstoneEvent(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? {
|
||||
private fun formatRoomTombstoneEvent(event: Event, senderName: String?, isDm: Boolean): CharSequence? {
|
||||
return if (event.isSentByCurrentUser()) {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_update_by_you else R.string.notice_room_update_by_you)
|
||||
sp.getString(if (isDm) R.string.notice_direct_room_update_by_you else R.string.notice_room_update_by_you)
|
||||
} else {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_update else R.string.notice_room_update, senderName)
|
||||
sp.getString(if (isDm) R.string.notice_direct_room_update else R.string.notice_room_update, senderName)
|
||||
}
|
||||
}
|
||||
|
||||
@ -272,20 +266,20 @@ class NoticeEventFormatter @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? {
|
||||
private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?, isDm: Boolean): CharSequence? {
|
||||
val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility ?: return null
|
||||
|
||||
val historyVisibilitySuffix = roomHistoryVisibilityFormatter.getNoticeSuffix(historyVisibility)
|
||||
return if (event.isSentByCurrentUser()) {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_made_future_direct_room_visibility_by_you else R.string.notice_made_future_room_visibility_by_you,
|
||||
sp.getString(if (isDm) R.string.notice_made_future_direct_room_visibility_by_you else R.string.notice_made_future_room_visibility_by_you,
|
||||
historyVisibilitySuffix)
|
||||
} else {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_made_future_direct_room_visibility else R.string.notice_made_future_room_visibility,
|
||||
sp.getString(if (isDm) R.string.notice_made_future_direct_room_visibility else R.string.notice_made_future_room_visibility,
|
||||
senderName, historyVisibilitySuffix)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatRoomThirdPartyInvite(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? {
|
||||
private fun formatRoomThirdPartyInvite(event: Event, senderName: String?, isDm: Boolean): CharSequence? {
|
||||
val content = event.getClearContent().toModel<RoomThirdPartyInviteContent>()
|
||||
val prevContent = event.resolvedPrevContent()?.toModel<RoomThirdPartyInviteContent>()
|
||||
|
||||
@ -294,24 +288,24 @@ class NoticeEventFormatter @Inject constructor(
|
||||
// Revoke case
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(
|
||||
if (rs.isDm()) {
|
||||
if (isDm) {
|
||||
R.string.notice_direct_room_third_party_revoked_invite_by_you
|
||||
} else {
|
||||
R.string.notice_room_third_party_revoked_invite_by_you
|
||||
},
|
||||
prevContent.displayName)
|
||||
} else {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_third_party_revoked_invite else R.string.notice_room_third_party_revoked_invite,
|
||||
sp.getString(if (isDm) R.string.notice_direct_room_third_party_revoked_invite else R.string.notice_room_third_party_revoked_invite,
|
||||
senderName, prevContent.displayName)
|
||||
}
|
||||
}
|
||||
content != null -> {
|
||||
// Invitation case
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_third_party_invite_by_you else R.string.notice_room_third_party_invite_by_you,
|
||||
sp.getString(if (isDm) R.string.notice_direct_room_third_party_invite_by_you else R.string.notice_room_third_party_invite_by_you,
|
||||
content.displayName)
|
||||
} else {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_third_party_invite else R.string.notice_room_third_party_invite,
|
||||
sp.getString(if (isDm) R.string.notice_direct_room_third_party_invite else R.string.notice_room_third_party_invite,
|
||||
senderName, content.displayName)
|
||||
}
|
||||
}
|
||||
@ -358,7 +352,7 @@ class NoticeEventFormatter @Inject constructor(
|
||||
}
|
||||
EventType.CALL_REJECT ->
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.call_tile_you_declined, "")
|
||||
sp.getString(R.string.call_tile_you_declined_this_call)
|
||||
} else {
|
||||
sp.getString(R.string.call_tile_other_declined, senderName)
|
||||
}
|
||||
@ -366,13 +360,13 @@ class NoticeEventFormatter @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatRoomMemberEvent(event: Event, senderName: String?, rs: RoomSummary?): String? {
|
||||
private fun formatRoomMemberEvent(event: Event, senderName: String?, isDm: Boolean): String? {
|
||||
val eventContent: RoomMemberContent? = event.getClearContent().toModel()
|
||||
val prevEventContent: RoomMemberContent? = event.resolvedPrevContent().toModel()
|
||||
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
|
||||
|| eventContent?.membership == Membership.LEAVE
|
||||
return if (isMembershipEvent) {
|
||||
buildMembershipNotice(event, senderName, eventContent, prevEventContent, rs)
|
||||
buildMembershipNotice(event, senderName, eventContent, prevEventContent, isDm)
|
||||
} else {
|
||||
buildProfileNotice(event, senderName, eventContent, prevEventContent)
|
||||
}
|
||||
@ -554,25 +548,25 @@ class NoticeEventFormatter @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatRoomGuestAccessEvent(event: Event, senderName: String?, rs: RoomSummary?): String? {
|
||||
private fun formatRoomGuestAccessEvent(event: Event, senderName: String?, isDm: Boolean): String? {
|
||||
val eventContent: RoomGuestAccessContent? = event.getClearContent().toModel()
|
||||
return when (eventContent?.guestAccess) {
|
||||
GuestAccess.CanJoin ->
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(
|
||||
if (rs.isDm()) R.string.notice_direct_room_guest_access_can_join_by_you else R.string.notice_room_guest_access_can_join_by_you
|
||||
if (isDm) R.string.notice_direct_room_guest_access_can_join_by_you else R.string.notice_room_guest_access_can_join_by_you
|
||||
)
|
||||
} else {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_guest_access_can_join else R.string.notice_room_guest_access_can_join,
|
||||
sp.getString(if (isDm) R.string.notice_direct_room_guest_access_can_join else R.string.notice_room_guest_access_can_join,
|
||||
senderName)
|
||||
}
|
||||
GuestAccess.Forbidden ->
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(
|
||||
if (rs.isDm()) R.string.notice_direct_room_guest_access_forbidden_by_you else R.string.notice_room_guest_access_forbidden_by_you
|
||||
if (isDm) R.string.notice_direct_room_guest_access_forbidden_by_you else R.string.notice_room_guest_access_forbidden_by_you
|
||||
)
|
||||
} else {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_guest_access_forbidden else R.string.notice_room_guest_access_forbidden,
|
||||
sp.getString(if (isDm) R.string.notice_direct_room_guest_access_forbidden else R.string.notice_room_guest_access_forbidden,
|
||||
senderName)
|
||||
}
|
||||
else -> null
|
||||
@ -656,7 +650,7 @@ class NoticeEventFormatter @Inject constructor(
|
||||
senderName: String?,
|
||||
eventContent: RoomMemberContent?,
|
||||
prevEventContent: RoomMemberContent?,
|
||||
rs: RoomSummary?): String? {
|
||||
isDm: Boolean): String? {
|
||||
val senderDisplayName = senderName ?: event.senderId ?: ""
|
||||
val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: event.stateKey ?: ""
|
||||
return when (eventContent?.membership) {
|
||||
@ -706,17 +700,17 @@ class NoticeEventFormatter @Inject constructor(
|
||||
Membership.JOIN ->
|
||||
eventContent.safeReason?.let { reason ->
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_join_with_reason_by_you else R.string.notice_room_join_with_reason_by_you,
|
||||
sp.getString(if (isDm) R.string.notice_direct_room_join_with_reason_by_you else R.string.notice_room_join_with_reason_by_you,
|
||||
reason)
|
||||
} else {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_join_with_reason else R.string.notice_room_join_with_reason,
|
||||
sp.getString(if (isDm) R.string.notice_direct_room_join_with_reason else R.string.notice_room_join_with_reason,
|
||||
senderDisplayName, reason)
|
||||
}
|
||||
} ?: run {
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_join_by_you else R.string.notice_room_join_by_you)
|
||||
sp.getString(if (isDm) R.string.notice_direct_room_join_by_you else R.string.notice_room_join_by_you)
|
||||
} else {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_join else R.string.notice_room_join,
|
||||
sp.getString(if (isDm) R.string.notice_direct_room_join else R.string.notice_room_join,
|
||||
senderDisplayName)
|
||||
}
|
||||
}
|
||||
@ -738,7 +732,7 @@ class NoticeEventFormatter @Inject constructor(
|
||||
eventContent.safeReason?.let { reason ->
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(
|
||||
if (rs.isDm()) {
|
||||
if (isDm) {
|
||||
R.string.notice_direct_room_leave_with_reason_by_you
|
||||
} else {
|
||||
R.string.notice_room_leave_with_reason_by_you
|
||||
@ -746,14 +740,14 @@ class NoticeEventFormatter @Inject constructor(
|
||||
reason
|
||||
)
|
||||
} else {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_leave_with_reason else R.string.notice_room_leave_with_reason,
|
||||
sp.getString(if (isDm) R.string.notice_direct_room_leave_with_reason else R.string.notice_room_leave_with_reason,
|
||||
senderDisplayName, reason)
|
||||
}
|
||||
} ?: run {
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_leave_by_you else R.string.notice_room_leave_by_you)
|
||||
sp.getString(if (isDm) R.string.notice_direct_room_leave_by_you else R.string.notice_room_leave_by_you)
|
||||
} else {
|
||||
sp.getString(if (rs.isDm()) R.string.notice_direct_room_leave else R.string.notice_room_leave,
|
||||
sp.getString(if (isDm) R.string.notice_direct_room_leave else R.string.notice_room_leave,
|
||||
senderDisplayName)
|
||||
}
|
||||
}
|
||||
@ -818,14 +812,14 @@ class NoticeEventFormatter @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatJoinRulesEvent(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? {
|
||||
private fun formatJoinRulesEvent(event: Event, senderName: String?, isDm: Boolean): CharSequence? {
|
||||
val content = event.getClearContent().toModel<RoomJoinRulesContent>() ?: return null
|
||||
return when (content.joinRules) {
|
||||
RoomJoinRules.INVITE ->
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(if (rs.isDm()) R.string.direct_room_join_rules_invite_by_you else R.string.room_join_rules_invite_by_you)
|
||||
sp.getString(if (isDm) R.string.direct_room_join_rules_invite_by_you else R.string.room_join_rules_invite_by_you)
|
||||
} else {
|
||||
sp.getString(if (rs.isDm()) R.string.direct_room_join_rules_invite else R.string.room_join_rules_invite,
|
||||
sp.getString(if (isDm) R.string.direct_room_join_rules_invite else R.string.room_join_rules_invite,
|
||||
senderName)
|
||||
}
|
||||
RoomJoinRules.PUBLIC ->
|
||||
|
@ -34,6 +34,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
@ -48,7 +49,6 @@ import javax.inject.Inject
|
||||
* This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline
|
||||
*/
|
||||
class MessageInformationDataFactory @Inject constructor(private val session: Session,
|
||||
private val roomSummariesHolder: RoomSummariesHolder,
|
||||
private val dateFormatter: VectorDateFormatter,
|
||||
private val visibilityHelper: TimelineEventVisibilityHelper,
|
||||
private val vectorPreferences: VectorPreferences) {
|
||||
@ -74,7 +74,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
||||
|| nextDisplayableEvent.isEdition()
|
||||
|
||||
val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE)
|
||||
val e2eDecoration = getE2EDecoration(event)
|
||||
val roomSummary = params.partialState.roomSummary
|
||||
val e2eDecoration = getE2EDecoration(roomSummary, event)
|
||||
|
||||
// SendState Decoration
|
||||
val isSentByMe = event.root.senderId == session.myUserId
|
||||
@ -140,8 +141,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
||||
}
|
||||
}
|
||||
|
||||
private fun getE2EDecoration(event: TimelineEvent): E2EDecoration {
|
||||
val roomSummary = roomSummariesHolder.get(event.roomId)
|
||||
private fun getE2EDecoration(roomSummary: RoomSummary?, event: TimelineEvent): E2EDecoration {
|
||||
return if (
|
||||
event.root.sendState == SendState.SYNCED
|
||||
&& roomSummary?.isEncrypted.orFalse()
|
||||
|
@ -1,43 +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.features.home.room.detail.timeline.helper
|
||||
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/*
|
||||
You can use this to share room summary instances within the app.
|
||||
You should probably use this only in the context of the timeline
|
||||
*/
|
||||
@Singleton
|
||||
class RoomSummariesHolder @Inject constructor() {
|
||||
|
||||
private var roomSummaries = HashMap<String, RoomSummary>()
|
||||
|
||||
fun set(roomSummary: RoomSummary) {
|
||||
roomSummaries[roomSummary.roomId] = roomSummary
|
||||
}
|
||||
|
||||
fun get(roomId: String) = roomSummaries[roomId]
|
||||
|
||||
fun remove(roomId: String) = roomSummaries.remove(roomId)
|
||||
|
||||
fun clear() {
|
||||
roomSummaries.clear()
|
||||
}
|
||||
}
|
@ -19,12 +19,8 @@ package im.vector.app.features.home.room.detail.timeline.helper
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.VisibilityState
|
||||
import im.vector.app.core.epoxy.LoadingItem_
|
||||
import im.vector.app.core.epoxy.TimelineEmptyItem_
|
||||
import im.vector.app.core.resources.UserPreferencesProvider
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import im.vector.app.features.home.room.detail.UnreadState
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
|
||||
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
|
||||
@ -34,9 +30,7 @@ import kotlin.reflect.KMutableProperty0
|
||||
private const val DEFAULT_PREFETCH_THRESHOLD = 30
|
||||
|
||||
class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMutableProperty0<Int?>,
|
||||
private val adapterPositionMapping: MutableMap<String, Int>,
|
||||
private val userPreferencesProvider: UserPreferencesProvider,
|
||||
private val callManager: WebRtcCallManager
|
||||
private val adapterPositionMapping: MutableMap<String, Int>
|
||||
) {
|
||||
|
||||
private var previousModelsSize = 0
|
||||
@ -50,14 +44,12 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
|
||||
) {
|
||||
positionOfReadMarker.set(null)
|
||||
adapterPositionMapping.clear()
|
||||
val callIds = mutableSetOf<String>()
|
||||
|
||||
// Add some prefetch loader if needed
|
||||
models.addBackwardPrefetchIfNeeded(timeline, callback)
|
||||
models.addForwardPrefetchIfNeeded(timeline, callback)
|
||||
|
||||
val modelsIterator = models.listIterator()
|
||||
val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents()
|
||||
var index = 0
|
||||
val firstUnreadEventId = (unreadState as? UnreadState.HasUnread)?.firstUnreadEventId
|
||||
var atLeastOneVisibleItemSinceLastDaySeparator = false
|
||||
@ -83,11 +75,6 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
|
||||
return@forEach
|
||||
}
|
||||
atLeastOneVisibleItemSinceLastDaySeparator = false
|
||||
} else if (epoxyModel is CallTileTimelineItem) {
|
||||
val hasBeenRemoved = modelsIterator.removeCallItemIfNeeded(epoxyModel, callIds, showHiddenEvents)
|
||||
if (!hasBeenRemoved) {
|
||||
atLeastOneVisibleItemSinceLastDaySeparator = true
|
||||
}
|
||||
}
|
||||
if (appendReadMarker) {
|
||||
modelsIterator.addReadMarkerItem(callback)
|
||||
@ -109,29 +96,6 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
|
||||
add(readMarker)
|
||||
}
|
||||
|
||||
private fun MutableListIterator<EpoxyModel<*>>.removeCallItemIfNeeded(
|
||||
epoxyModel: CallTileTimelineItem,
|
||||
callIds: MutableSet<String>,
|
||||
showHiddenEvents: Boolean
|
||||
): Boolean {
|
||||
val callId = epoxyModel.attributes.callId
|
||||
// We should remove the call tile if we already have one for this call or
|
||||
// if this is an active call tile without an actual call (which can happen with permalink)
|
||||
val shouldRemoveCallItem = callIds.contains(callId)
|
||||
|| (!callManager.getAdvertisedCalls().contains(callId) && epoxyModel.attributes.callStatus.isActive())
|
||||
val removed = shouldRemoveCallItem && !showHiddenEvents
|
||||
if (removed) {
|
||||
remove()
|
||||
val emptyItem = TimelineEmptyItem_()
|
||||
.id(epoxyModel.id())
|
||||
.eventId(epoxyModel.attributes.informationData.eventId)
|
||||
.notBlank(false)
|
||||
add(emptyItem)
|
||||
}
|
||||
callIds.add(callId)
|
||||
return removed
|
||||
}
|
||||
|
||||
private fun MutableList<EpoxyModel<*>>.addBackwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) {
|
||||
val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false
|
||||
if (shouldAddBackwardPrefetch) {
|
||||
|
@ -0,0 +1,133 @@
|
||||
/*
|
||||
* 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.home.room.detail.timeline.helper
|
||||
|
||||
import im.vector.app.core.utils.TextUtils
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetContent
|
||||
import org.threeten.bp.Duration
|
||||
|
||||
class TimelineEventsGroup(val groupId: String) {
|
||||
|
||||
val events: Set<TimelineEvent>
|
||||
get() = _events
|
||||
|
||||
private val _events = HashSet<TimelineEvent>()
|
||||
|
||||
fun add(timelineEvent: TimelineEvent) {
|
||||
_events.add(timelineEvent)
|
||||
}
|
||||
}
|
||||
|
||||
class TimelineEventsGroups {
|
||||
|
||||
private val groups = HashMap<String, TimelineEventsGroup>()
|
||||
|
||||
fun addOrIgnore(event: TimelineEvent) {
|
||||
val groupId = event.getGroupIdOrNull() ?: return
|
||||
groups.getOrPut(groupId) { TimelineEventsGroup(groupId) }.add(event)
|
||||
}
|
||||
|
||||
fun getOrNull(event: TimelineEvent): TimelineEventsGroup? {
|
||||
val groupId = event.getGroupIdOrNull() ?: return null
|
||||
return groups[groupId]
|
||||
}
|
||||
|
||||
private fun TimelineEvent.getGroupIdOrNull(): String? {
|
||||
val type = root.getClearType()
|
||||
val content = root.getClearContent()
|
||||
return if (EventType.isCallEvent(type)) {
|
||||
(content?.get("call_id") as? String)
|
||||
} else if (type == EventType.STATE_ROOM_WIDGET || type == EventType.STATE_ROOM_WIDGET_LEGACY) {
|
||||
root.stateKey
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
groups.clear()
|
||||
}
|
||||
}
|
||||
|
||||
class JitsiWidgetEventsGroup(private val group: TimelineEventsGroup) {
|
||||
|
||||
val callId: String = group.groupId
|
||||
|
||||
fun isStillActive(): Boolean {
|
||||
return group.events.none {
|
||||
it.root.getClearContent().toModel<WidgetContent>()?.isActive() == false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CallSignalingEventsGroup(private val group: TimelineEventsGroup) {
|
||||
|
||||
val callId: String = group.groupId
|
||||
|
||||
fun isVideo(): Boolean {
|
||||
val invite = getInvite() ?: return false
|
||||
return invite.root.getClearContent().toModel<CallInviteContent>()?.isVideo().orFalse()
|
||||
}
|
||||
|
||||
fun isRinging(): Boolean {
|
||||
return getAnswer() == null && getHangup() == null && getReject() == null
|
||||
}
|
||||
|
||||
fun isInCall(): Boolean {
|
||||
return getHangup() == null && getReject() == null
|
||||
}
|
||||
|
||||
fun formattedDuration(): String {
|
||||
val start = getAnswer()?.root?.originServerTs
|
||||
val end = getHangup()?.root?.originServerTs
|
||||
return if (start == null || end == null) {
|
||||
""
|
||||
} else {
|
||||
val durationInMillis = (end - start).coerceAtLeast(0L)
|
||||
val duration = Duration.ofMillis(durationInMillis)
|
||||
TextUtils.formatDuration(duration)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there are only events from one side.
|
||||
*/
|
||||
fun callWasMissed(): Boolean {
|
||||
return group.events.distinctBy { it.senderInfo.userId }.size == 1
|
||||
}
|
||||
|
||||
private fun getAnswer(): TimelineEvent? {
|
||||
return group.events.firstOrNull { it.root.getClearType() == EventType.CALL_ANSWER }
|
||||
}
|
||||
|
||||
private fun getInvite(): TimelineEvent? {
|
||||
return group.events.firstOrNull { it.root.getClearType() == EventType.CALL_INVITE }
|
||||
}
|
||||
|
||||
private fun getHangup(): TimelineEvent? {
|
||||
return group.events.firstOrNull { it.root.getClearType() == EventType.CALL_HANGUP }
|
||||
}
|
||||
|
||||
private fun getReject(): TimelineEvent? {
|
||||
return group.events.firstOrNull { it.root.getClearType() == EventType.CALL_REJECT }
|
||||
}
|
||||
}
|
@ -42,6 +42,10 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
||||
override val baseAttributes: AbsBaseMessageItem.Attributes
|
||||
get() = attributes
|
||||
|
||||
override fun isCacheable(): Boolean {
|
||||
return attributes.informationData.sendStateDecoration != SendStateDecoration.SENT
|
||||
}
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var attributes: Attributes
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
@ -31,13 +32,11 @@ import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.ClickListener
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.core.extensions.setLeftDrawable
|
||||
import im.vector.app.core.extensions.setTextWithColoredPart
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.RoomDetailAction
|
||||
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import timber.log.Timber
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state)
|
||||
abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Holder>() {
|
||||
@ -45,6 +44,8 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
|
||||
override val baseAttributes: AbsBaseMessageItem.Attributes
|
||||
get() = attributes
|
||||
|
||||
override fun isCacheable() = false
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var attributes: Attributes
|
||||
|
||||
@ -57,81 +58,190 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
|
||||
}
|
||||
holder.creatorNameView.text = attributes.userOfInterest.getBestName()
|
||||
attributes.avatarRenderer.render(attributes.userOfInterest, holder.creatorAvatarView)
|
||||
if (attributes.callKind != CallKind.UNKNOWN) {
|
||||
holder.callKindView.isVisible = true
|
||||
holder.callKindView.setText(attributes.callKind.title)
|
||||
holder.callKindView.setLeftDrawable(attributes.callKind.icon)
|
||||
} else {
|
||||
holder.callKindView.isVisible = false
|
||||
when (attributes.callStatus) {
|
||||
CallStatus.INVITED -> renderInvitedStatus(holder)
|
||||
CallStatus.IN_CALL -> renderInCallStatus(holder)
|
||||
CallStatus.REJECTED -> renderRejectedStatus(holder)
|
||||
CallStatus.ENDED -> renderEndedStatus(holder)
|
||||
CallStatus.MISSED -> renderMissedStatus(holder)
|
||||
}
|
||||
if (attributes.callStatus == CallStatus.INVITED && !attributes.informationData.sentByMe && attributes.isStillActive) {
|
||||
holder.acceptRejectViewGroup.isVisible = true
|
||||
holder.acceptView.onClick {
|
||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.AcceptCall(callId = attributes.callId))
|
||||
renderSendState(holder.view, null, holder.failedToSendIndicator)
|
||||
}
|
||||
|
||||
private fun renderMissedStatus(holder: Holder) {
|
||||
// Sent by me means I made the call and opponent missed it.
|
||||
if (attributes.informationData.sentByMe) {
|
||||
if (attributes.callKind.isVoiceCall) {
|
||||
holder.statusView.setStatus(R.string.call_tile_no_answer, R.drawable.ic_voice_call_declined)
|
||||
} else {
|
||||
holder.statusView.setStatus(R.string.call_tile_no_answer, R.drawable.ic_video_call_declined)
|
||||
}
|
||||
holder.rejectView.setLeftDrawable(R.drawable.ic_call_hangup, R.attr.colorOnPrimary)
|
||||
holder.rejectView.onClick {
|
||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.EndCall)
|
||||
} else {
|
||||
if (attributes.callKind.isVoiceCall) {
|
||||
holder.statusView.setStatus(R.string.call_tile_voice_missed, R.drawable.ic_missed_voice_call_small)
|
||||
} else {
|
||||
holder.statusView.setStatus(R.string.call_tile_video_missed, R.drawable.ic_missed_video_call_small)
|
||||
}
|
||||
holder.statusView.isVisible = false
|
||||
when (attributes.callKind) {
|
||||
CallKind.CONFERENCE -> {
|
||||
holder.rejectView.setText(R.string.ignore)
|
||||
holder.acceptView.setText(R.string.join)
|
||||
holder.acceptView.setLeftDrawable(R.drawable.ic_call_audio_small, R.attr.colorOnPrimary)
|
||||
}
|
||||
holder.acceptRejectViewGroup.isVisible = true
|
||||
holder.acceptView.setText(R.string.call_tile_call_back)
|
||||
holder.acceptView.setLeftDrawable(attributes.callKind.icon, R.attr.colorOnPrimary)
|
||||
holder.acceptView.onClick {
|
||||
val callbackAction = RoomDetailAction.StartCall(attributes.callKind == CallKind.VIDEO)
|
||||
attributes.callback?.onTimelineItemAction(callbackAction)
|
||||
}
|
||||
holder.rejectView.isVisible = false
|
||||
}
|
||||
|
||||
private fun renderEndedStatus(holder: Holder) {
|
||||
holder.acceptRejectViewGroup.isVisible = false
|
||||
when (attributes.callKind) {
|
||||
CallKind.VIDEO -> {
|
||||
val endCallStatus = holder.resources.getString(R.string.call_tile_video_call_has_ended, attributes.formattedDuration)
|
||||
holder.statusView.setStatus(endCallStatus)
|
||||
}
|
||||
CallKind.AUDIO -> {
|
||||
val endCallStatus = holder.resources.getString(R.string.call_tile_voice_call_has_ended, attributes.formattedDuration)
|
||||
holder.statusView.setStatus(endCallStatus)
|
||||
}
|
||||
CallKind.CONFERENCE -> {
|
||||
holder.statusView.setStatus(R.string.call_tile_ended)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderRejectedStatus(holder: Holder) {
|
||||
holder.acceptRejectViewGroup.isVisible = true
|
||||
holder.acceptView.setText(R.string.call_tile_call_back)
|
||||
holder.acceptView.setLeftDrawable(attributes.callKind.icon, R.attr.colorOnPrimary)
|
||||
holder.acceptView.onClick {
|
||||
val callbackAction = RoomDetailAction.StartCall(attributes.callKind == CallKind.VIDEO)
|
||||
attributes.callback?.onTimelineItemAction(callbackAction)
|
||||
}
|
||||
holder.rejectView.isVisible = false
|
||||
// Sent by me means I rejected the call made by opponent.
|
||||
if (attributes.informationData.sentByMe) {
|
||||
if (attributes.callKind.isVoiceCall) {
|
||||
holder.statusView.setStatus(R.string.call_tile_voice_declined, R.drawable.ic_voice_call_declined)
|
||||
} else {
|
||||
holder.statusView.setStatus(R.string.call_tile_video_declined, R.drawable.ic_video_call_declined)
|
||||
}
|
||||
} else {
|
||||
if (attributes.callKind.isVoiceCall) {
|
||||
holder.statusView.setStatus(R.string.call_tile_no_answer, R.drawable.ic_voice_call_declined)
|
||||
} else {
|
||||
holder.statusView.setStatus(R.string.call_tile_no_answer, R.drawable.ic_video_call_declined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderInCallStatus(holder: Holder) {
|
||||
holder.acceptRejectViewGroup.isVisible = true
|
||||
holder.acceptView.isVisible = false
|
||||
when {
|
||||
attributes.callKind == CallKind.CONFERENCE -> {
|
||||
holder.rejectView.isVisible = true
|
||||
holder.rejectView.setText(R.string.leave)
|
||||
holder.rejectView.setLeftDrawable(R.drawable.ic_call_hangup, R.attr.colorOnPrimary)
|
||||
holder.rejectView.onClick {
|
||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.LeaveJitsiCall)
|
||||
}
|
||||
CallKind.AUDIO -> {
|
||||
}
|
||||
attributes.isStillActive -> {
|
||||
holder.rejectView.isVisible = true
|
||||
holder.rejectView.setText(R.string.call_notification_hangup)
|
||||
holder.rejectView.setLeftDrawable(R.drawable.ic_call_hangup, R.attr.colorOnPrimary)
|
||||
holder.rejectView.onClick {
|
||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.EndCall)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
holder.acceptRejectViewGroup.isVisible = false
|
||||
}
|
||||
}
|
||||
if (attributes.callKind.isVoiceCall) {
|
||||
holder.statusView.setStatus(R.string.call_tile_voice_active)
|
||||
} else {
|
||||
holder.statusView.setStatus(R.string.call_tile_video_active)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderInvitedStatus(holder: Holder) {
|
||||
when {
|
||||
attributes.callKind == CallKind.CONFERENCE -> {
|
||||
holder.acceptRejectViewGroup.isVisible = true
|
||||
holder.acceptView.onClick {
|
||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.JoinJitsiCall)
|
||||
}
|
||||
holder.acceptView.isVisible = true
|
||||
holder.rejectView.isVisible = false
|
||||
holder.acceptView.setText(R.string.join)
|
||||
holder.acceptView.setLeftDrawable(R.drawable.ic_call_video_small, R.attr.colorOnPrimary)
|
||||
}
|
||||
!attributes.informationData.sentByMe && attributes.isStillActive -> {
|
||||
holder.acceptRejectViewGroup.isVisible = true
|
||||
holder.acceptView.isVisible = true
|
||||
holder.rejectView.isVisible = true
|
||||
holder.acceptView.onClick {
|
||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.AcceptCall(callId = attributes.callId))
|
||||
}
|
||||
holder.rejectView.setLeftDrawable(R.drawable.ic_call_hangup, R.attr.colorOnPrimary)
|
||||
holder.rejectView.onClick {
|
||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.EndCall)
|
||||
}
|
||||
if (attributes.callKind == CallKind.AUDIO) {
|
||||
holder.rejectView.setText(R.string.call_notification_reject)
|
||||
holder.acceptView.setText(R.string.call_notification_answer)
|
||||
holder.acceptView.setLeftDrawable(R.drawable.ic_call_audio_small, R.attr.colorOnPrimary)
|
||||
}
|
||||
CallKind.VIDEO -> {
|
||||
} else if (attributes.callKind == CallKind.VIDEO) {
|
||||
holder.rejectView.setText(R.string.call_notification_reject)
|
||||
holder.acceptView.setText(R.string.call_notification_answer)
|
||||
holder.acceptView.setLeftDrawable(R.drawable.ic_call_video_small, R.attr.colorOnPrimary)
|
||||
}
|
||||
else -> {
|
||||
Timber.w("Shouldn't be in that state")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
holder.acceptRejectViewGroup.isVisible = false
|
||||
holder.statusView.isVisible = true
|
||||
else -> {
|
||||
holder.acceptRejectViewGroup.isVisible = false
|
||||
}
|
||||
}
|
||||
when {
|
||||
// Invite state for conference should show as InCallStatus
|
||||
attributes.callKind == CallKind.CONFERENCE -> {
|
||||
holder.statusView.setStatus(R.string.call_tile_video_active)
|
||||
}
|
||||
attributes.informationData.sentByMe -> {
|
||||
holder.statusView.setStatus(R.string.call_ringing)
|
||||
}
|
||||
attributes.callKind.isVoiceCall -> {
|
||||
holder.statusView.setStatus(R.string.call_tile_voice_incoming)
|
||||
}
|
||||
else -> {
|
||||
holder.statusView.setStatus(R.string.call_tile_video_incoming)
|
||||
}
|
||||
}
|
||||
holder.statusView.setCallStatus(attributes)
|
||||
renderSendState(holder.view, null, holder.failedToSendIndicator)
|
||||
}
|
||||
|
||||
private fun TextView.setCallStatus(attributes: Attributes) {
|
||||
when (attributes.callStatus) {
|
||||
CallStatus.INVITED -> if (attributes.informationData.sentByMe) {
|
||||
setText(R.string.call_tile_you_started_call)
|
||||
} else {
|
||||
text = context.getString(R.string.call_tile_other_started_call, attributes.userOfInterest.getBestName())
|
||||
}
|
||||
CallStatus.IN_CALL -> setText(R.string.call_tile_in_call)
|
||||
CallStatus.REJECTED -> if (attributes.informationData.sentByMe) {
|
||||
setTextWithColoredPart(R.string.call_tile_you_declined, R.string.call_tile_call_back) {
|
||||
val callbackAction = RoomDetailAction.StartCall(attributes.callKind == CallKind.VIDEO)
|
||||
attributes.callback?.onTimelineItemAction(callbackAction)
|
||||
}
|
||||
} else {
|
||||
text = context.getString(R.string.call_tile_other_declined, attributes.userOfInterest.getBestName())
|
||||
}
|
||||
CallStatus.ENDED -> setText(R.string.call_tile_ended)
|
||||
}
|
||||
private fun TextView.setStatus(@StringRes statusRes: Int, @DrawableRes drawableRes: Int? = null) {
|
||||
val status = resources.getString(statusRes)
|
||||
setStatus(status, drawableRes)
|
||||
}
|
||||
|
||||
private fun TextView.setStatus(status: String, @DrawableRes drawableRes: Int? = null) {
|
||||
setLeftDrawable(drawableRes ?: attributes.callKind.icon)
|
||||
text = status
|
||||
}
|
||||
|
||||
class Holder : AbsBaseMessageItem.Holder(STUB_ID) {
|
||||
val acceptView by bind<Button>(R.id.itemCallAcceptView)
|
||||
val rejectView by bind<Button>(R.id.itemCallRejectView)
|
||||
val acceptRejectViewGroup by bind<ViewGroup>(R.id.itemCallAcceptRejectViewGroup)
|
||||
val callKindView by bind<TextView>(R.id.itemCallKindTextView)
|
||||
val creatorAvatarView by bind<ImageView>(R.id.itemCallCreatorAvatar)
|
||||
val creatorNameView by bind<TextView>(R.id.itemCallCreatorNameTextView)
|
||||
val statusView by bind<TextView>(R.id.itemCallStatusTextView)
|
||||
val endGuideline by bind<View>(R.id.messageEndGuideline)
|
||||
val failedToSendIndicator by bind<ImageView>(R.id.messageFailToSendIndicator)
|
||||
|
||||
val resources: Resources
|
||||
get() = view.context.resources
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -144,6 +254,7 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
|
||||
val callStatus: CallStatus,
|
||||
val userOfInterest: MatrixItem,
|
||||
val isStillActive: Boolean,
|
||||
val formattedDuration: String,
|
||||
val callback: TimelineEventController.Callback? = null,
|
||||
override val informationData: MessageInformationData,
|
||||
override val avatarRenderer: AvatarRenderer,
|
||||
@ -157,14 +268,17 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
|
||||
enum class CallKind(@DrawableRes val icon: Int, @StringRes val title: Int) {
|
||||
VIDEO(R.drawable.ic_call_video_small, R.string.action_video_call),
|
||||
AUDIO(R.drawable.ic_call_audio_small, R.string.action_voice_call),
|
||||
CONFERENCE(R.drawable.ic_call_conference_small, R.string.conference_call_in_progress),
|
||||
UNKNOWN(0, 0)
|
||||
CONFERENCE(R.drawable.ic_call_video_small, R.string.action_video_call);
|
||||
|
||||
val isVoiceCall
|
||||
get() = this == AUDIO
|
||||
}
|
||||
|
||||
enum class CallStatus {
|
||||
INVITED,
|
||||
IN_CALL,
|
||||
REJECTED,
|
||||
MISSED,
|
||||
ENDED;
|
||||
|
||||
fun isActive() = this == INVITED || this == IN_CALL
|
||||
|
@ -26,4 +26,9 @@ interface ItemWithEvents {
|
||||
fun canAppendReadMarker(): Boolean = true
|
||||
|
||||
fun isVisible(): Boolean = true
|
||||
|
||||
/**
|
||||
* Returns false if you want epoxy controller to rebuild the event each time a built is triggered
|
||||
*/
|
||||
fun isCacheable(): Boolean = true
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
|
||||
var latestEventTime: CharSequence = ""
|
||||
val latestEvent = roomSummary.latestPreviewableEvent
|
||||
if (latestEvent != null) {
|
||||
latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect.not())
|
||||
latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect, roomSummary.isDirect.not())
|
||||
latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
|
||||
}
|
||||
val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers)
|
||||
|
@ -21,6 +21,7 @@ import im.vector.app.R
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
|
||||
import im.vector.app.features.home.room.detail.timeline.format.NoticeEventFormatter
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
@ -135,7 +136,7 @@ class NotifiableEventResolver @Inject constructor(
|
||||
if (room == null) {
|
||||
Timber.e("## Unable to resolve room for eventId [$event]")
|
||||
// Ok room is not known in store, but we can still display something
|
||||
val body = displayableEventFormatter.format(event, false)
|
||||
val body = displayableEventFormatter.format(event, isDm = false, appendAuthor = false)
|
||||
val roomName = stringProvider.getString(R.string.notification_unknown_room_name)
|
||||
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
|
||||
|
||||
@ -168,7 +169,7 @@ class NotifiableEventResolver @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
val body = displayableEventFormatter.format(event, false).toString()
|
||||
val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString()
|
||||
val roomName = room.roomSummary()?.displayName ?: ""
|
||||
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
|
||||
|
||||
@ -209,7 +210,7 @@ class NotifiableEventResolver @Inject constructor(
|
||||
val roomId = event.roomId ?: return null
|
||||
val dName = event.senderId?.let { session.getRoomMember(it, roomId)?.displayName }
|
||||
if (Membership.INVITE == content.membership) {
|
||||
val body = noticeEventFormatter.format(event, dName, session.getRoomSummary(roomId))
|
||||
val body = noticeEventFormatter.format(event, dName, isDm = session.getRoomSummary(roomId)?.isDirect.orFalse())
|
||||
?: stringProvider.getString(R.string.notification_new_invitation)
|
||||
return InviteNotifiableEvent(
|
||||
session.myUserId,
|
||||
|
@ -360,7 +360,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||
val builder = NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
|
||||
.setContentTitle(ensureTitleNotEmpty(title))
|
||||
.apply {
|
||||
setContentText(stringProvider.getString(R.string.call_ring))
|
||||
setContentText(stringProvider.getString(R.string.call_ringing))
|
||||
if (call.mxCall.isVideoCall) {
|
||||
setSmallIcon(R.drawable.ic_call_answer_video)
|
||||
} else {
|
||||
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.popup
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.View
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.glide.GlideApp
|
||||
import im.vector.app.databinding.AlerterJitsiCallLayoutBinding
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
class JitsiCallAlert(uid: String,
|
||||
override val shouldBeDisplayedIn: ((Activity) -> Boolean) = { true }
|
||||
) : DefaultVectorAlert(uid, "", "", 0, shouldBeDisplayedIn) {
|
||||
|
||||
override val priority = PopupAlertManager.JITSI_CALL_PRIORITY
|
||||
override val layoutRes = R.layout.alerter_jitsi_call_layout
|
||||
override var colorAttribute: Int? = R.attr.colorSurface
|
||||
override val dismissOnClick: Boolean = false
|
||||
override val isLight: Boolean = true
|
||||
|
||||
class ViewBinder(private val matrixItem: MatrixItem?,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val onJoin: () -> Unit) : VectorAlert.ViewBinder {
|
||||
|
||||
override fun bind(view: View) {
|
||||
val views = AlerterJitsiCallLayoutBinding.bind(view)
|
||||
views.jitsiCallNameView.text = matrixItem?.getBestName()
|
||||
matrixItem?.let { avatarRenderer.render(it, views.jitsiCallAvatar, GlideApp.with(view.context.applicationContext)) }
|
||||
views.jitsiCallJoinView.setOnClickListener {
|
||||
onJoin()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -44,6 +44,7 @@ class PopupAlertManager @Inject constructor() {
|
||||
|
||||
companion object {
|
||||
const val INCOMING_CALL_PRIORITY = Int.MAX_VALUE
|
||||
const val JITSI_CALL_PRIORITY = INCOMING_CALL_PRIORITY - 1
|
||||
}
|
||||
|
||||
private var weakCurrentActivity: WeakReference<Activity>? = null
|
||||
|
15
vector/src/main/res/drawable/ic_call_audio_settings.xml
Normal file
15
vector/src/main/res/drawable/ic_call_audio_settings.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="30dp"
|
||||
android:height="26dp"
|
||||
android:viewportWidth="30"
|
||||
android:viewportHeight="26">
|
||||
<path
|
||||
android:pathData="M14.9623,0.7826L7.5,7.0012L1.875,7.0012C0.8395,7.0012 0,7.8406 0,8.8762V17.1262C0,18.1617 0.8395,19.0012 1.875,19.0012L7.5,19.0012L14.9623,25.2198C15.5729,25.7286 16.5,25.2944 16.5,24.4996V1.5028C16.5,0.7079 15.5729,0.2737 14.9623,0.7826Z"
|
||||
android:fillColor="#737D8C"/>
|
||||
<path
|
||||
android:pathData="M26.486,3.2332C26.0621,2.6883 25.2768,2.5902 24.7318,3.014C24.1875,3.4374 24.089,4.2214 24.5112,4.7663L24.5121,4.7675L24.5133,4.7691L24.5304,4.7918C24.547,4.8141 24.5737,4.8504 24.609,4.9002C24.6798,4.9999 24.785,5.153 24.9134,5.3548C25.1706,5.759 25.5181,6.3541 25.8665,7.1007C26.5669,8.6014 27.2493,10.6674 27.2493,13.0007C27.2493,15.3339 26.5669,17.4 25.8665,18.9006C25.5181,19.6472 25.1706,20.2424 24.9134,20.6465C24.785,20.8483 24.6798,21.0015 24.609,21.1011C24.5737,21.1509 24.547,21.1873 24.5304,21.2095L24.5133,21.2322L24.5121,21.2338L24.511,21.2353C24.089,21.7801 24.1876,22.5641 24.7318,22.9874C25.2768,23.4112 26.0621,23.313 26.486,22.7681L25.5544,22.0435C26.486,22.7681 26.486,22.7681 26.486,22.7681L26.4884,22.7649L26.492,22.7602L26.5024,22.7467L26.5355,22.7027C26.5629,22.6659 26.6007,22.6144 26.6473,22.5486C26.7406,22.4173 26.8697,22.2289 27.0226,21.9887C27.3279,21.509 27.7304,20.8184 28.132,19.9579C28.9317,18.2442 29.7493,15.8103 29.7493,13.0007C29.7493,10.191 28.9317,7.7571 28.132,6.0435C27.7304,5.1829 27.3279,4.4924 27.0226,4.0126C26.8697,3.7724 26.7406,3.5841 26.6473,3.4527C26.6007,3.387 26.5629,3.3354 26.5355,3.2987L26.5024,3.2547L26.492,3.2411L26.4884,3.2365L26.4871,3.2347C26.4871,3.2347 26.486,3.2332 25.4993,4.0007L26.486,3.2332Z"
|
||||
android:fillColor="#737D8C"/>
|
||||
<path
|
||||
android:pathData="M21.9871,7.7335C21.5632,7.1886 20.7779,7.0904 20.2329,7.5143C19.6894,7.937 19.5904,8.7193 20.0104,9.2641L20.0147,9.2698C20.0202,9.2773 20.0308,9.2917 20.0457,9.3126C20.0754,9.3545 20.1221,9.4223 20.1802,9.5136C20.2967,9.6967 20.4567,9.9705 20.6176,10.3153C20.943,11.0124 21.2504,11.9534 21.2504,13.001C21.2504,14.0485 20.943,14.9895 20.6176,15.6866C20.4567,16.0314 20.2967,16.3052 20.1802,16.4883C20.1221,16.5796 20.0754,16.6474 20.0457,16.6893C20.0308,16.7102 20.0202,16.7246 20.0147,16.7321L20.0104,16.7378C19.5904,17.2826 19.6894,18.0649 20.2329,18.4876C20.7779,18.9115 21.5632,18.8133 21.9871,18.2684L21.0004,17.5009C21.9871,18.2684 21.9871,18.2684 21.9871,18.2684L21.9893,18.2655L21.992,18.2619L21.9992,18.2526L22.0198,18.2252C22.0362,18.2032 22.0578,18.1736 22.084,18.1368C22.1363,18.0632 22.2068,17.9602 22.2893,17.8305C22.454,17.5718 22.669,17.2026 22.8831,16.7438C23.3078,15.8338 23.7504,14.5249 23.7504,13.001C23.7504,11.477 23.3078,10.1681 22.8831,9.2581C22.669,8.7993 22.454,8.4302 22.2893,8.1714C22.2068,8.0417 22.1363,7.9387 22.084,7.8651C22.0578,7.8282 22.0362,7.7987 22.0198,7.7766L21.9992,7.7493L21.992,7.74L21.9893,7.7364L21.9881,7.7349C21.9881,7.7349 21.9871,7.7335 21.0004,8.5009L21.9871,7.7335Z"
|
||||
android:fillColor="#737D8C"/>
|
||||
</vector>
|
10
vector/src/main/res/drawable/ic_call_back_to_chat.xml
Normal file
10
vector/src/main/res/drawable/ic_call_back_to_chat.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M22.8,12C22.8,17.9646 17.9647,22.8 12,22.8C10.0252,22.8 8.1742,22.2699 6.5815,21.3444L2.4145,22.6302C1.6455,22.8675 0.9255,22.1455 1.1649,21.3772L2.4903,17.1235C1.6672,15.599 1.2,13.854 1.2,12C1.2,6.0353 6.0353,1.1999 12,1.1999C17.9647,1.1999 22.8,6.0353 22.8,12ZM8.4,12C8.4,12.6627 7.8628,13.2 7.2,13.2C6.5373,13.2 6,12.6627 6,12C6,11.3372 6.5373,10.8 7.2,10.8C7.8628,10.8 8.4,11.3372 8.4,12ZM12,13.2C12.6628,13.2 13.2,12.6627 13.2,12C13.2,11.3372 12.6628,10.8 12,10.8C11.3373,10.8 10.8,11.3372 10.8,12C10.8,12.6627 11.3373,13.2 12,13.2ZM18,12C18,12.6627 17.4628,13.2 16.8,13.2C16.1373,13.2 15.6,12.6627 15.6,12C15.6,11.3372 16.1373,10.8 16.8,10.8C17.4628,10.8 18,11.3372 18,12Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
@ -1,11 +0,0 @@
|
||||
<vector android:autoMirrored="true" android:height="40dp"
|
||||
android:viewportHeight="40" android:viewportWidth="40"
|
||||
android:width="40dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M20,20m-20,0a20,20 0,1 1,40 0a20,20 0,1 1,-40 0"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
<path android:fillColor="#FFFFFF" android:fillType="nonZero"
|
||||
android:pathData="M13.25,17.75L13.25,22.25L16.25,22.25L20,26L20,14L16.25,17.75L13.25,17.75ZM23.375,20C23.375,18.6725 22.61,17.5325 21.5,16.9775L21.5,23.015C22.61,22.4675 23.375,21.3275 23.375,20ZM21.5,13.4225L21.5,14.9675C23.6675,15.6125 25.25,17.6225 25.25,20C25.25,22.3775 23.6675,24.3875 21.5,25.0325L21.5,26.5775C24.5075,25.895 26.75,23.21 26.75,20C26.75,16.79 24.5075,14.105 21.5,13.4225Z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
</vector>
|
@ -1,7 +0,0 @@
|
||||
<vector android:autoMirrored="true" android:height="18dp"
|
||||
android:viewportHeight="18" android:viewportWidth="18"
|
||||
android:width="18dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFF" android:fillType="nonZero"
|
||||
android:pathData="M2.25,6.75L2.25,11.25L5.25,11.25L9,15L9,3L5.25,6.75L2.25,6.75ZM12.375,9C12.375,7.6725 11.61,6.5325 10.5,5.9775L10.5,12.015C11.61,11.4675 12.375,10.3275 12.375,9ZM10.5,2.4225L10.5,3.9675C12.6675,4.6125 14.25,6.6225 14.25,9C14.25,11.3775 12.6675,13.3875 10.5,14.0325L10.5,15.5775C13.5075,14.895 15.75,12.21 15.75,9C15.75,5.79 13.5075,3.105 10.5,2.4225Z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
</vector>
|
@ -9,4 +9,5 @@
|
||||
<path
|
||||
android:pathData="M12.6666,3.9999L14.3753,2.633C15.03,2.1092 16,2.5754 16,3.4139V8.586C16,9.4245 15.03,9.8906 14.3753,9.3668L12.6666,7.9999V3.9999Z"
|
||||
android:fillColor="#737D8C"/>
|
||||
|
||||
</vector>
|
||||
|
@ -1,11 +0,0 @@
|
||||
<vector android:autoMirrored="true" android:height="40dp"
|
||||
android:viewportHeight="40" android:viewportWidth="40"
|
||||
android:width="40dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M20,20m-20,0a20,20 0,1 1,40 0a20,20 0,1 1,-40 0"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
<path android:fillColor="#FFFFFF" android:fillType="nonZero"
|
||||
android:pathData="M26.75,15.875L23.75,18.875L23.75,16.25C23.75,15.8375 23.4125,15.5 23,15.5L18.365,15.5L26.75,23.885L26.75,15.875ZM13.4525,12.5L12.5,13.4525L14.5475,15.5L14,15.5C13.5875,15.5 13.25,15.8375 13.25,16.25L13.25,23.75C13.25,24.1625 13.5875,24.5 14,24.5L23,24.5C23.1575,24.5 23.2925,24.44 23.405,24.365L25.7975,26.75L26.75,25.7975L13.4525,12.5Z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
</vector>
|
@ -1,7 +0,0 @@
|
||||
<vector android:autoMirrored="true" android:height="18dp"
|
||||
android:viewportHeight="18" android:viewportWidth="18"
|
||||
android:width="18dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFF" android:fillType="nonZero"
|
||||
android:pathData="M15.75,4.875L12.75,7.875L12.75,5.25C12.75,4.8375 12.4125,4.5 12,4.5L7.365,4.5L15.75,12.885L15.75,4.875ZM2.4525,1.5L1.5,2.4525L3.5475,4.5L3,4.5C2.5875,4.5 2.25,4.8375 2.25,5.25L2.25,12.75C2.25,13.1625 2.5875,13.5 3,13.5L12,13.5C12.1575,13.5 12.2925,13.44 12.405,13.365L14.7975,15.75L15.75,14.7975L2.4525,1.5Z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
</vector>
|
10
vector/src/main/res/drawable/ic_mic_off.xml
Normal file
10
vector/src/main/res/drawable/ic_mic_off.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,11h-1.7c0,0.74 -0.16,1.43 -0.43,2.05l1.23,1.23c0.56,-0.98 0.9,-2.09 0.9,-3.28zM14.98,11.17c0,-0.06 0.02,-0.11 0.02,-0.17L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v0.18l5.98,5.99zM4.27,3L3,4.27l6.01,6.01L9.01,11c0,1.66 1.33,3 2.99,3 0.22,0 0.44,-0.03 0.65,-0.08l1.66,1.66c-0.71,0.33 -1.5,0.52 -2.31,0.52 -2.76,0 -5.3,-2.1 -5.3,-5.1L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c0.91,-0.13 1.77,-0.45 2.54,-0.9L19.73,21 21,19.73 4.27,3z"/>
|
||||
</vector>
|
10
vector/src/main/res/drawable/ic_mic_on.xml
Normal file
10
vector/src/main/res/drawable/ic_mic_on.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>
|
||||
</vector>
|
@ -1,41 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="25dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="25">
|
||||
<path
|
||||
android:pathData="M1,2L23,24"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M15,10.34V5C15.0007,4.256 14.725,3.5383 14.2264,2.9862C13.7277,2.4341 13.0417,2.0869 12.3015,2.0122C11.5613,1.9374 10.8197,2.1403 10.2207,2.5816C9.6217,3.0228 9.208,3.6709 9.06,4.4M9,10V13C9.0005,13.593 9.1768,14.1725 9.5064,14.6653C9.8361,15.1582 10.3045,15.5423 10.8523,15.7691C11.4002,15.996 12.0029,16.0554 12.5845,15.9399C13.1661,15.8243 13.7005,15.539 14.12,15.12L9,10Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M16.9999,17.95C16.0237,18.9464 14.7721,19.6285 13.4056,19.9086C12.039,20.1887 10.62,20.0542 9.3304,19.5223C8.0409,18.9903 6.9397,18.0853 6.1681,16.9232C5.3965,15.761 4.9897,14.3949 4.9999,13V11M18.9999,11V13C18.9996,13.4124 18.9628,13.824 18.8899,14.23"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12,20V24"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M8,24H16"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
@ -1,10 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,0C10.9391,0 9.9217,0.4214 9.1716,1.1716C8.4214,1.9217 8,2.9391 8,4V12C8,13.0609 8.4214,14.0783 9.1716,14.8284C9.9217,15.5786 10.9391,16 12,16C13.0609,16 14.0783,15.5786 14.8284,14.8284C15.5786,14.0783 16,13.0609 16,12V4C16,2.9391 15.5786,1.9217 14.8284,1.1716C14.0783,0.4214 13.0609,0 12,0ZM10.5858,2.5858C10.9609,2.2107 11.4696,2 12,2C12.5304,2 13.0391,2.2107 13.4142,2.5858C13.7893,2.9609 14,3.4696 14,4V12C14,12.5304 13.7893,13.0391 13.4142,13.4142C13.0391,13.7893 12.5304,14 12,14C11.4696,14 10.9609,13.7893 10.5858,13.4142C10.2107,13.0391 10,12.5304 10,12V4C10,3.4696 10.2107,2.9609 10.5858,2.5858ZM6,10C6,9.4477 5.5523,9 5,9C4.4477,9 4,9.4477 4,10V12C4,14.1217 4.8429,16.1566 6.3432,17.6569C7.6058,18.9195 9.247,19.7165 11,19.9373V22H8C7.4477,22 7,22.4477 7,23C7,23.5523 7.4477,24 8,24H12H16C16.5523,24 17,23.5523 17,23C17,22.4477 16.5523,22 16,22H13V19.9373C14.753,19.7165 16.3942,18.9195 17.6569,17.6569C19.1571,16.1566 20,14.1217 20,12V10C20,9.4477 19.5523,9 19,9C18.4477,9 18,9.4477 18,10V12C18,13.5913 17.3679,15.1174 16.2426,16.2426C15.1174,17.3679 13.5913,18 12,18C10.4087,18 8.8826,17.3679 7.7574,16.2426C6.6321,15.1174 6,13.5913 6,12V10Z"
|
||||
android:fillColor="#2E2F32"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M0,10V17.7778C0,19 1,20 2.2222,20H4.4444C5.6667,20 6.6667,19 6.6667,17.7778V13.3333C6.6667,12.1111 5.6667,11.1111 4.4444,11.1111H2.2222V10C2.2222,5.7 5.7,2.2222 10,2.2222C14.3,2.2222 17.7778,5.7 17.7778,10V11.1111H15.5556C14.3333,11.1111 13.3333,12.1111 13.3333,13.3333V17.7778C13.3333,19 14.3333,20 15.5556,20H17.7778C19,20 20,19 20,17.7778V10C20,4.4778 15.5222,0 10,0C4.4778,0 0,4.4778 0,10Z"
|
||||
android:fillColor="#737D8C"/>
|
||||
</vector>
|
9
vector/src/main/res/drawable/ic_sound_device_phone.xml
Normal file
9
vector/src/main/res/drawable/ic_sound_device_phone.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="14dp"
|
||||
android:height="22dp"
|
||||
android:viewportWidth="14"
|
||||
android:viewportHeight="22">
|
||||
<path
|
||||
android:pathData="M12,0.01L2,0C0.9,0 0,0.9 0,2V20C0,21.1 0.9,22 2,22H12C13.1,22 14,21.1 14,20V2C14,0.9 13.1,0.01 12,0.01ZM12,18H2V4H12V18Z"
|
||||
android:fillColor="#737D8C"/>
|
||||
</vector>
|
15
vector/src/main/res/drawable/ic_sound_device_speaker.xml
Normal file
15
vector/src/main/res/drawable/ic_sound_device_speaker.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M11.9699,0.226L6,5.2009L1.5,5.2009C0.6716,5.2009 0,5.8725 0,6.7009V13.3009C0,14.1294 0.6716,14.8009 1.5,14.8009L6,14.8009L11.9699,19.7758C12.4584,20.1829 13.2,19.8355 13.2,19.1996V0.8022C13.2,0.1663 12.4584,-0.181 11.9699,0.226Z"
|
||||
android:fillColor="#737D8C"/>
|
||||
<path
|
||||
android:pathData="M21.1888,2.1866C20.8497,1.7507 20.2214,1.6721 19.7855,2.0112C19.35,2.3499 19.2712,2.9771 19.6089,3.413L19.6097,3.414L19.6107,3.4153L19.6243,3.4335C19.6376,3.4513 19.6589,3.4803 19.6872,3.5202C19.7438,3.5999 19.828,3.7224 19.9308,3.8839C20.1365,4.2072 20.4145,4.6833 20.6932,5.2806C21.2535,6.4811 21.7994,8.134 21.7994,10.0005C21.7994,11.8671 21.2535,13.52 20.6932,14.7205C20.4145,15.3178 20.1365,15.7939 19.9308,16.1172C19.828,16.2786 19.7438,16.4012 19.6872,16.4809C19.6589,16.5208 19.6376,16.5498 19.6243,16.5676L19.6107,16.5858L19.6097,16.5871L19.6088,16.5882C19.2712,17.0241 19.3501,17.6512 19.7855,17.9899C20.2214,18.329 20.8497,18.2504 21.1888,17.8145L20.4435,17.2348C21.1888,17.8145 21.1888,17.8145 21.1888,17.8145L21.1908,17.8119L21.1936,17.8082L21.2019,17.7974L21.2284,17.7621C21.2503,17.7327 21.2805,17.6915 21.3179,17.6389C21.3925,17.5338 21.4958,17.3832 21.6181,17.191C21.8623,16.8072 22.1843,16.2547 22.5056,15.5663C23.1453,14.1954 23.7994,12.2482 23.7994,10.0005C23.7994,7.7528 23.1453,5.8057 22.5056,4.4348C22.1843,3.7463 21.8623,3.1939 21.6181,2.8101C21.4958,2.6179 21.3925,2.4673 21.3179,2.3622C21.2805,2.3096 21.2503,2.2683 21.2284,2.2389L21.2019,2.2037L21.1936,2.1929L21.1908,2.1892L21.1897,2.1877C21.1897,2.1877 21.1888,2.1866 20.3994,2.8005L21.1888,2.1866Z"
|
||||
android:fillColor="#737D8C"/>
|
||||
<path
|
||||
android:pathData="M17.5896,5.7868C17.2506,5.3509 16.6223,5.2723 16.1864,5.6114C15.7515,5.9496 15.6723,6.5755 16.0083,7.0113L16.0117,7.0159C16.0162,7.0219 16.0246,7.0333 16.0365,7.0501C16.0603,7.0836 16.0977,7.1378 16.1441,7.2108C16.2374,7.3574 16.3654,7.5764 16.4941,7.8522C16.7544,8.4099 17.0003,9.1628 17.0003,10.0008C17.0003,10.8388 16.7544,11.5916 16.4941,12.1493C16.3654,12.4251 16.2374,12.6441 16.1441,12.7907C16.0977,12.8637 16.0603,12.9179 16.0365,12.9514C16.0246,12.9682 16.0162,12.9797 16.0117,12.9857L16.0083,12.9903C15.6723,13.4261 15.7515,14.0519 16.1864,14.3901C16.6223,14.7292 17.2506,14.6506 17.5896,14.2147L16.8003,13.6008C17.5896,14.2147 17.5896,14.2147 17.5896,14.2147L17.5914,14.2124L17.5936,14.2095L17.5994,14.2021L17.6158,14.1802C17.6289,14.1626 17.6463,14.1389 17.6672,14.1094C17.709,14.0505 17.7654,13.9682 17.8315,13.8644C17.9632,13.6574 18.1352,13.3621 18.3065,12.9951C18.6462,12.267 19.0003,11.2199 19.0003,10.0008C19.0003,8.7816 18.6462,7.7345 18.3065,7.0065C18.1352,6.6394 17.9632,6.3441 17.8315,6.1371C17.7654,6.0333 17.709,5.951 17.6672,5.8921C17.6463,5.8626 17.6289,5.8389 17.6158,5.8213L17.5994,5.7995L17.5936,5.792L17.5914,5.7891L17.5905,5.7879C17.5905,5.7879 17.5896,5.7868 16.8003,6.4008L17.5896,5.7868Z"
|
||||
android:fillColor="#737D8C"/>
|
||||
</vector>
|
12
vector/src/main/res/drawable/ic_sound_device_wireless.xml
Normal file
12
vector/src/main/res/drawable/ic_sound_device_wireless.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M10.8817,3.1593L5.4545,7.682L1.3636,7.682C0.6105,7.682 0,8.2925 0,9.0456V15.0456C0,15.7987 0.6105,16.4092 1.3636,16.4092L5.4545,16.4092L10.8817,20.9318C11.3258,21.3019 12,20.9861 12,20.4081V3.6831C12,3.1051 11.3258,2.7893 10.8817,3.1593Z"
|
||||
android:fillColor="#737D8C"/>
|
||||
<path
|
||||
android:pathData="M23.6656,7.8241L20.0887,4.2472C19.5634,3.7219 18.6629,4.0888 18.6629,4.8309V9.9836L15.4195,6.7402C15.0943,6.415 14.5691,6.415 14.2439,6.7402C13.9187,7.0654 13.9187,7.5907 14.2439,7.9159L18.3211,11.993L14.2439,16.0703C13.9187,16.3954 13.9187,16.9207 14.2439,17.2459C14.5691,17.5711 15.0943,17.5711 15.4195,17.2459L18.6629,14.0025V19.1553C18.6629,19.8973 19.5634,20.2725 20.0887,19.7472L23.6656,16.162C23.9908,15.8368 23.9908,15.3115 23.6656,14.9863L20.6724,11.993L23.6656,9.0081C23.9908,8.6829 23.9908,8.1493 23.6656,7.8241ZM20.3305,6.8486L21.898,8.4161L20.3305,9.9836V6.8486ZM21.898,15.57L20.3305,17.1375V14.0025L21.898,15.57Z"
|
||||
android:fillColor="#737D8C"/>
|
||||
</vector>
|
@ -1,10 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="25dp"
|
||||
android:height="25dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="25"
|
||||
android:viewportHeight="25">
|
||||
<path
|
||||
android:pathData="M0.5,7.5C0.5,5.8432 1.8432,4.5 3.5,4.5H14.5C16.1569,4.5 17.5,5.8432 17.5,7.5V17.5C17.5,19.1569 16.1569,20.5 14.5,20.5H3.5C1.8432,20.5 0.5,19.1569 0.5,17.5V7.5Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M19.5,9.5L22.8753,6.7998C23.5301,6.2759 24.5,6.7421 24.5,7.5806V17.4194C24.5,18.2579 23.5301,18.7241 22.8753,18.2002L19.5,15.5V9.5Z"
|
||||
|
10
vector/src/main/res/drawable/ic_video_call_declined.xml
Normal file
10
vector/src/main/res/drawable/ic_video_call_declined.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="12dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="12">
|
||||
<path
|
||||
android:pathData="M0,2.8182C0,1.7638 0.8954,0.9091 2,0.9091H9.3333C10.4379,0.9091 11.3333,1.7638 11.3333,2.8182V9.1818C11.3333,10.2361 10.4379,11.0909 9.3333,11.0909H2C0.8954,11.0909 0,10.2361 0,9.1818V2.8182ZM12.6667,4.0909L14.9169,2.3726C15.3534,2.0392 16,2.3359 16,2.8695V9.1305C16,9.6641 15.3534,9.9607 14.9169,9.6274L12.6667,7.9091V4.0909ZM7.8233,3.8154C7.965,3.9571 7.965,4.1869 7.8233,4.3285L6.1798,5.972L7.8937,7.6859C8.0354,7.8276 8.0354,8.0574 7.8937,8.1991C7.752,8.3408 7.5223,8.3408 7.3805,8.1991L5.6667,6.4852L3.9528,8.1991C3.8111,8.3408 3.5813,8.3408 3.4396,8.1991C3.2979,8.0574 3.2979,7.8276 3.4396,7.6859L5.1535,5.972L3.51,4.3285C3.3683,4.1869 3.3683,3.9571 3.51,3.8154C3.6517,3.6737 3.8815,3.6737 4.0232,3.8154L5.6667,5.4589L7.3102,3.8154C7.4519,3.6737 7.6816,3.6737 7.8233,3.8154Z"
|
||||
android:fillColor="#737D8C"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
@ -1,20 +1,7 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M10.66,5H14C14.5304,5 15.0391,5.2107 15.4142,5.5858C15.7893,5.9609 16,6.4696 16,7V10.34L17,11.34L23,7V17M16,16V17C16,17.5304 15.7893,18.0391 15.4142,18.4142C15.0391,18.7893 14.5304,19 14,19H3C2.4696,19 1.9609,18.7893 1.5858,18.4142C1.2107,18.0391 1,17.5304 1,17V7C1,6.4696 1.2107,5.9609 1.5858,5.5858C1.9609,5.2107 2.4696,5 3,5H5L16,16Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M1,1L23,23"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<vector android:height="24dp" android:viewportHeight="32"
|
||||
android:viewportWidth="32" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<path android:fillColor="#FF000000" android:pathData="M22.5,20.9l5,3C27.6,24 27.8,24 28,24c0.2,0 0.3,0 0.5,-0.1c0.3,-0.2 0.5,-0.5 0.5,-0.9V9c0,-0.4 -0.2,-0.7 -0.5,-0.9c-0.3,-0.2 -0.7,-0.2 -1,0l-5,3C22.2,11.3 22,11.6 22,12v8C22,20.4 22.2,20.7 22.5,20.9z"/>
|
||||
<path android:fillColor="#FF000000" android:pathData="M29.7,28.3L20,18.6V11c0,-1.7 -1.3,-3 -3,-3H9.4L3.7,2.3c-0.4,-0.4 -1,-0.4 -1.4,0s-0.4,1 0,1.4l26,26c0.2,0.2 0.5,0.3 0.7,0.3s0.5,-0.1 0.7,-0.3C30.1,29.3 30.1,28.7 29.7,28.3z"/>
|
||||
<path android:fillColor="#FF000000" android:pathData="M3,11v10c0,1.7 1.3,3 3,3h11c0.8,0 1.5,-0.3 2,-0.8L4.3,8.5C3.5,9.1 3,10 3,11z"/>
|
||||
</vector>
|
||||
|
@ -1,5 +0,0 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M17,10.5V7c0,-0.55 -0.45,-1 -1,-1H4c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1v-3.5l4,4v-11l-4,4z"/>
|
||||
</vector>
|
12
vector/src/main/res/drawable/ic_voice_call_declined.xml
Normal file
12
vector/src/main/res/drawable/ic_voice_call_declined.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="17dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="17"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M5.8514,10.6408C6.6121,11.4621 8.4433,12.8842 8.9424,13.176C8.9719,13.1933 9.0057,13.2134 9.0435,13.2358C9.8051,13.6886 12.1916,15.1072 13.9304,13.7796C15.2775,12.751 14.8395,11.5939 14.386,11.25C14.0756,11.0085 13.161,10.3429 12.3005,9.7434C11.4555,9.1548 10.9846,9.6264 10.6662,9.9453C10.6603,9.9512 10.6545,9.957 10.6488,9.9627L10.0081,10.6034C9.845,10.7665 9.5968,10.707 9.3591,10.5203C8.5062,9.8707 7.8788,9.2439 7.5649,8.93L7.5623,8.9273C7.2484,8.6135 6.6293,7.9938 5.9798,7.1409C5.7931,6.9032 5.7335,6.655 5.8967,6.4919L6.5373,5.8512C6.5431,5.8455 6.5489,5.8397 6.5547,5.8338C6.8736,5.5154 7.3453,5.0445 6.7566,4.1995C6.1571,3.339 5.4915,2.4244 5.2501,2.114C4.9061,1.6606 3.749,1.2226 2.7204,2.5697C1.3928,4.3085 2.8115,6.6949 3.2642,7.4565C3.2867,7.4943 3.3068,7.5281 3.324,7.5576C3.6159,8.0567 5.0301,9.8801 5.8514,10.6408Z"
|
||||
android:fillColor="#737D8C"/>
|
||||
<path
|
||||
android:pathData="M14.2982,2.0522C14.4601,1.8877 14.4601,1.6211 14.2982,1.4567C14.1362,1.2923 13.8736,1.2923 13.7117,1.4567L11.8334,3.3637L9.9551,1.4567C9.7932,1.2923 9.5306,1.2923 9.3687,1.4567C9.2067,1.6211 9.2067,1.8877 9.3687,2.0522L11.2469,3.9592L9.2882,5.9479C9.1263,6.1124 9.1263,6.379 9.2882,6.5434C9.4502,6.7078 9.7127,6.7078 9.8747,6.5434L11.8334,4.5546L13.7921,6.5434C13.9541,6.7078 14.2167,6.7078 14.3786,6.5434C14.5406,6.379 14.5406,6.1124 14.3786,5.9479L12.4199,3.9592L14.2982,2.0522Z"
|
||||
android:fillColor="#737D8C"/>
|
||||
</vector>
|
@ -22,60 +22,103 @@
|
||||
|
||||
<org.webrtc.SurfaceViewRenderer
|
||||
android:id="@+id/fullscreenRenderer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/pipContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/callControlsView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/callToolbar">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/pipRendererWrapper"
|
||||
android:layout_width="@dimen/call_pip_width"
|
||||
android:layout_height="@dimen/call_pip_height"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_goneMarginEnd="0dp"
|
||||
app:cardCornerRadius="@dimen/call_pip_radius"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<org.webrtc.SurfaceViewRenderer
|
||||
android:id="@+id/pipRenderer"
|
||||
android:layout_width="@dimen/call_pip_width"
|
||||
android:layout_height="@dimen/call_pip_height"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/otherKnownCallLayout"
|
||||
android:layout_width="@dimen/call_pip_width"
|
||||
android:layout_height="@dimen/call_pip_height"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@color/element_background_light"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:visibility="gone"
|
||||
app:cardBackgroundColor="@color/bg_call_screen"
|
||||
app:cardCornerRadius="@dimen/call_pip_radius"
|
||||
app:cardElevation="4dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/pipRendererWrapper"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/otherKnownCallAvatarView"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_gravity="center"
|
||||
android:importantForAccessibility="no"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/otherSmallIsHeldIcon"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_call_small_pause" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/callToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<org.webrtc.SurfaceViewRenderer
|
||||
android:id="@+id/pipRenderer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="144dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/otherKnownCallLayout"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="144dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@color/element_background_light"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:visibility="gone"
|
||||
app:cardBackgroundColor="@color/bg_call_screen"
|
||||
app:cardCornerRadius="4dp"
|
||||
app:cardElevation="4dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:fitsSystemWindows="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/otherKnownCallAvatarView"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_gravity="center"
|
||||
android:importantForAccessibility="no"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/otherSmallIsHeldIcon"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_call_small_pause" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
app:navigationIcon="@drawable/ic_back_24dp"
|
||||
app:navigationIconTint="@color/element_background_light"
|
||||
app:subtitle="3:10"
|
||||
app:subtitleTextAppearance="@style/TextAppearance.Vector.Caption"
|
||||
app:subtitleTextColor="@color/element_background_light"
|
||||
app:title="Video call"
|
||||
app:titleMarginTop="16dp"
|
||||
app:titleTextAppearance="@style/TextAppearance.Vector.Body.Medium"
|
||||
app:titleTextColor="@color/element_background_light" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/otherMemberAvatar"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="120dp"
|
||||
android:contentDescription="@string/avatar"
|
||||
android:importantForAccessibility="no"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
@ -111,22 +154,6 @@
|
||||
app:layout_constraintTop_toBottomOf="@id/otherMemberAvatar"
|
||||
tools:text="@sample/users.json/data/displayName" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/callStatusText"
|
||||
style="@style/Widget.Vector.TextView.Body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:gravity="center"
|
||||
android:textColor="@android:color/white"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/participantNameText"
|
||||
tools:text="@string/call_connecting" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/callActionText"
|
||||
style="@style/Widget.Vector.Button.Text"
|
||||
@ -137,35 +164,15 @@
|
||||
android:textColor="?colorSecondary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/callStatusText"
|
||||
app:layout_constraintTop_toBottomOf="@id/participantNameText"
|
||||
tools:text="@string/call_resume_action" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/callConnectingProgress"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_margin="8dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/callStatusText"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/callInfoGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="visible"
|
||||
app:constraint_referenced_ids="participantNameText, otherMemberAvatar,callStatusText" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/callVideoGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="visible"
|
||||
app:constraint_referenced_ids="pipRenderer, fullscreenRenderer"
|
||||
tools:visibility="invisible" />
|
||||
app:constraint_referenced_ids="participantNameText, otherMemberAvatar" />
|
||||
|
||||
<im.vector.app.features.call.CallControlsView
|
||||
android:id="@+id/callControlsView"
|
||||
@ -173,16 +180,4 @@
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/hud_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/call_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
70
vector/src/main/res/layout/alerter_jitsi_call_layout.xml
Normal file
70
vector/src/main/res/layout/alerter_jitsi_call_layout.xml
Normal file
@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp"
|
||||
tools:style="@style/AlertStyle">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/jitsiCallAvatar"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_margin="12dp"
|
||||
android:contentDescription="@string/call_notification_answer"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@sample/user_round_avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/jitsiCallNameView"
|
||||
style="@style/Widget.Vector.TextView.Subtitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="?vctr_content_primary"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toStartOf="@+id/jitsiCallJoinView"
|
||||
app:layout_constraintStart_toEndOf="@id/jitsiCallAvatar"
|
||||
app:layout_constraintTop_toTopOf="@id/jitsiCallAvatar"
|
||||
tools:text="@sample/users.json/data/displayName" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/jitsiCallKindView"
|
||||
style="@style/Widget.Vector.TextView.Body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:drawablePadding="4dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/call_jitsi_started"
|
||||
android:textColor="?vctr_content_secondary"
|
||||
app:layout_constraintEnd_toStartOf="@+id/jitsiCallJoinView"
|
||||
app:layout_constraintStart_toStartOf="@id/jitsiCallNameView"
|
||||
app:layout_constraintTop_toBottomOf="@id/jitsiCallNameView" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/jitsiCallJoinView"
|
||||
style="@style/Widget.Vector.Button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:contentDescription="@string/call_notification_answer"
|
||||
android:drawableStart="@drawable/ic_call_video_small"
|
||||
android:padding="8dp"
|
||||
android:text="@string/join"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -7,16 +7,6 @@
|
||||
android:background="?colorSurface"
|
||||
android:orientation="vertical">
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/callControlsSoundDevice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="@string/call_select_sound_device"
|
||||
app:leftIcon="@drawable/ic_call_speaker_default"
|
||||
app:tint="?vctr_content_primary"
|
||||
app:titleTextColor="?vctr_content_primary"
|
||||
tools:actionDescription="Speaker" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/callControlsSwitchCamera"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout 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="match_parent"
|
||||
android:background="?colorSurface"
|
||||
android:orientation="vertical">
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/callControlsSwitchCamera"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="@string/call_switch_camera"
|
||||
app:leftIcon="@drawable/ic_video_flip"
|
||||
app:tint="?vctr_content_primary"
|
||||
app:titleTextColor="?vctr_content_primary"
|
||||
tools:actionDescription="Front" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/callControlsOpenDialPad"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="@string/call_dial_pad_title"
|
||||
app:leftIcon="@drawable/ic_call_dial_pad"
|
||||
app:tint="?vctr_content_primary"
|
||||
app:titleTextColor="?vctr_content_primary"
|
||||
tools:actionDescription="" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/callControlsToggleSDHD"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="@string/call_format_turn_hd_on"
|
||||
app:leftIcon="@drawable/ic_hd"
|
||||
app:tint="?vctr_content_primary"
|
||||
app:titleTextColor="?vctr_content_primary"
|
||||
tools:actionDescription="Front" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/callControlsToggleHoldResume"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="Hold/resume"
|
||||
app:leftIcon="@drawable/ic_call_hold_action"
|
||||
app:tint="?vctr_content_primary"
|
||||
app:titleTextColor="?vctr_content_primary"
|
||||
tools:actionDescription="" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/callControlsTransfer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="@string/call_transfer_title"
|
||||
app:leftIcon="@drawable/ic_call_transfer"
|
||||
app:tint="?vctr_content_primary"
|
||||
app:titleTextColor="?vctr_content_primary"
|
||||
tools:actionDescription="" />
|
||||
|
||||
</LinearLayout>
|
@ -11,6 +11,15 @@
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<im.vector.app.core.ui.views.CurrentCallsView
|
||||
android:id="@+id/currentCallsView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/homeKeysBackupBanner"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/groupToolbar"
|
||||
android:layout_width="match_parent"
|
||||
@ -128,41 +137,12 @@
|
||||
app:layout_constraintTop_toBottomOf="@id/syncStateView"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<im.vector.app.core.ui.views.CurrentCallsView
|
||||
android:id="@+id/activeCallView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/homeKeysBackupBanner"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/roomListContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"
|
||||
app:layout_constraintTop_toBottomOf="@+id/activeCallView" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/activeCallPiPWrap"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/activeCallView">
|
||||
|
||||
<org.webrtc.SurfaceViewRenderer
|
||||
android:id="@+id/activeCallPiP"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="120dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
app:layout_constraintTop_toBottomOf="@+id/homeKeysBackupBanner" />
|
||||
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/bottomNavigationView"
|
||||
|
@ -12,6 +12,14 @@
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<im.vector.app.core.ui.views.CurrentCallsView
|
||||
android:id="@+id/currentCallsView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/roomToolbar"
|
||||
android:layout_width="match_parent"
|
||||
@ -100,21 +108,15 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/appBarLayout" />
|
||||
|
||||
<im.vector.app.core.ui.views.CurrentCallsView
|
||||
android:id="@+id/activeCallView"
|
||||
<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:background="?android:colorBackground"
|
||||
android:minHeight="54dp"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintTop_toBottomOf="@id/syncStateView" />
|
||||
|
||||
<im.vector.app.core.ui.views.ActiveConferenceView
|
||||
android:id="@+id/activeConferenceView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/activeCallView"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/timelineRecyclerView"
|
||||
@ -124,7 +126,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
|
||||
@ -140,7 +142,7 @@
|
||||
app:closeIcon="@drawable/ic_close_24dp"
|
||||
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" />
|
||||
|
||||
|
||||
@ -205,27 +207,6 @@
|
||||
app:barrierDirection="top"
|
||||
app:constraint_referenced_ids="composerLayout,notificationAreaView, failedMessagesWarningView" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/activeCallPiPWrap"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/jumpToReadMarkerView">
|
||||
|
||||
<org.webrtc.SurfaceViewRenderer
|
||||
android:id="@+id/activeCallPiP"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="120dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<im.vector.app.core.platform.BadgeFloatingActionButton
|
||||
android:id="@+id/jumpToBottomView"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -20,41 +20,29 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:drawablePadding="6dp"
|
||||
android:gravity="center"
|
||||
android:textColor="?vctr_content_primary"
|
||||
android:textStyle="bold"
|
||||
tools:text="@sample/users.json/data/displayName" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemCallKindTextView"
|
||||
style="@style/Widget.Vector.TextView.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:drawablePadding="4dp"
|
||||
android:gravity="center"
|
||||
android:textColor="?vctr_content_primary"
|
||||
tools:text="@string/action_video_call" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemCallStatusTextView"
|
||||
style="@style/Widget.Vector.TextView.Body"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="center"
|
||||
android:textColor="?vctr_content_secondary"
|
||||
tools:text="@string/video_call_in_progress" />
|
||||
android:maxLines="3"
|
||||
android:drawablePadding="8dp"
|
||||
tools:drawableLeft="@drawable/ic_missed_video_call"
|
||||
tools:text="@string/call_tile_video_incoming" />
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
@ -80,7 +68,6 @@
|
||||
style="@style/Widget.Vector.Button.Destructive"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:minWidth="120dp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/itemCallAcceptView"
|
||||
|
@ -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>
|
@ -58,23 +58,25 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:paddingStart="32dp"
|
||||
android:paddingEnd="32dp"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:visibility="gone"
|
||||
tools:background="@color/password_strength_bar_low"
|
||||
tools:layout_marginTop="120dp"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/openChatIcon"
|
||||
android:layout_width="@dimen/layout_touch_size"
|
||||
android:layout_height="@dimen/layout_touch_size"
|
||||
android:id="@+id/audioSettingsIcon"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:background="@drawable/bg_rounded_button"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/a11y_open_chat"
|
||||
android:contentDescription="@string/call_select_sound_device"
|
||||
android:focusable="true"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/ic_call_pip"
|
||||
android:src="@drawable/ic_call_audio_settings"
|
||||
app:backgroundTint="?android:colorBackground"
|
||||
app:tint="?vctr_content_primary"
|
||||
tools:ignore="MissingConstraints,MissingPrefix" />
|
||||
|
||||
<ImageView
|
||||
@ -85,8 +87,23 @@
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/a11y_mute_microphone"
|
||||
android:focusable="true"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_microphone_off"
|
||||
android:padding="12dp"
|
||||
android:src="@drawable/ic_mic_off"
|
||||
app:backgroundTint="?android:colorBackground"
|
||||
app:tint="?vctr_content_primary"
|
||||
tools:ignore="MissingConstraints,MissingPrefix" />
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/videoToggleIcon"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:background="@drawable/bg_rounded_button"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/a11y_stop_camera"
|
||||
android:focusable="true"
|
||||
android:padding="12dp"
|
||||
android:src="@drawable/ic_video"
|
||||
app:backgroundTint="?android:colorBackground"
|
||||
app:tint="?vctr_content_primary"
|
||||
tools:ignore="MissingConstraints,MissingPrefix" />
|
||||
@ -105,24 +122,11 @@
|
||||
app:tint="?colorOnPrimary"
|
||||
tools:ignore="MissingConstraints,MissingPrefix" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/videoToggleIcon"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:background="@drawable/bg_rounded_button"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/a11y_stop_camera"
|
||||
android:focusable="true"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_call_videocam_off_default"
|
||||
app:backgroundTint="?android:colorBackground"
|
||||
app:tint="?vctr_content_primary"
|
||||
tools:ignore="MissingConstraints,MissingPrefix" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/moreIcon"
|
||||
android:layout_width="@dimen/layout_touch_size"
|
||||
android:layout_height="@dimen/layout_touch_size"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/bg_rounded_button"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/settings"
|
||||
@ -135,9 +139,10 @@
|
||||
<androidx.constraintlayout.helper.widget.Flow
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:constraint_referenced_ids="openChatIcon, muteIcon, endCallIcon,videoToggleIcon,moreIcon"
|
||||
app:constraint_referenced_ids="videoToggleIcon, audioSettingsIcon, muteIcon, endCallIcon, moreIcon"
|
||||
app:flow_horizontalGap="16dp"
|
||||
app:flow_horizontalStyle="packed"
|
||||
app:flow_wrapMode="chain"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
@ -6,39 +6,20 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?colorPrimary"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
tools:parentTag="android.widget.RelativeLayout">
|
||||
tools:parentTag="android.widget.FrameLayout">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/currentCallsInfo"
|
||||
style="@style/Widget.Vector.TextView.Body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toStartOf="@id/returnToCallButton"
|
||||
android:drawablePadding="10dp"
|
||||
android:gravity="center_vertical"
|
||||
android:gravity="center"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:text="@string/call_only_active"
|
||||
android:textColor="?colorOnPrimary"
|
||||
app:drawableStartCompat="@drawable/ic_call_answer"
|
||||
app:drawableTint="?colorOnPrimary" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/returnToCallButton"
|
||||
style="@style/Widget.Vector.Button.Text.OnPrimary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignTop="@+id/currentCallsInfo"
|
||||
android:layout_alignBottom="@+id/currentCallsInfo"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:gravity="center"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:text="@string/action_return"
|
||||
android:textStyle="bold" />
|
||||
android:textColor="?colorOnPrimary" />
|
||||
|
||||
</merge>
|
||||
|
19
vector/src/main/res/layout/view_join_conference.xml
Normal file
19
vector/src/main/res/layout/view_join_conference.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingEnd="4dp"
|
||||
android:paddingStart="4dp"
|
||||
android:layout_width="wrap_content">
|
||||
|
||||
<Button
|
||||
android:id="@+id/join_conference_button"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="32dp"
|
||||
app:iconPadding="0dp"
|
||||
app:iconGravity="textStart"
|
||||
app:icon="@drawable/ic_call_video_small"
|
||||
app:iconTint="@color/element_background_light" />
|
||||
|
||||
</FrameLayout>
|
127
vector/src/main/res/layout/view_remove_jitsi_widget.xml
Normal file
127
vector/src/main/res/layout/view_remove_jitsi_widget.xml
Normal file
@ -0,0 +1,127 @@
|
||||
<?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"
|
||||
tools:ignore="ContentDescription"
|
||||
app: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"
|
||||
tools:ignore="ContentDescription"
|
||||
app: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"
|
||||
tools:ignore="ContentDescription"
|
||||
app: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"
|
||||
tools:ignore="ContentDescription"
|
||||
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>
|
@ -37,14 +37,11 @@
|
||||
app:showAsAction="always"
|
||||
tools:visible="true" />
|
||||
|
||||
<item
|
||||
android:id="@+id/hangup_call"
|
||||
android:icon="@drawable/ic_call_end"
|
||||
android:title="@string/call_notification_hangup"
|
||||
android:visible="false"
|
||||
app:iconTint="?colorError"
|
||||
<item android:id="@+id/join_conference"
|
||||
android:title="@string/join"
|
||||
app:actionLayout="@layout/view_join_conference"
|
||||
app:showAsAction="always"
|
||||
tools:visible="true" />
|
||||
/>
|
||||
|
||||
<item
|
||||
android:id="@+id/open_matrix_apps"
|
||||
|
11
vector/src/main/res/menu/vector_call.xml
Normal file
11
vector/src/main/res/menu/vector_call.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_call_open_chat"
|
||||
app:showAsAction="always"
|
||||
android:icon="@drawable/ic_call_back_to_chat"
|
||||
android:title="@string/a11y_open_chat" />
|
||||
|
||||
</menu>
|
@ -728,6 +728,7 @@
|
||||
<string name="call">Call</string>
|
||||
<string name="call_connected">Call connected</string>
|
||||
<string name="call_connecting">Call connecting…</string>
|
||||
<string name="call_ringing">Call ringing…</string>
|
||||
<string name="call_ended">Call ended</string>
|
||||
<plurals name="missed_audio_call">
|
||||
<item quantity="one">Missed audio call</item>
|
||||
@ -743,6 +744,8 @@
|
||||
<string name="incoming_voice_call">Incoming Voice Call</string>
|
||||
<string name="call_in_progress">Call In Progress…</string>
|
||||
<string name="video_call_in_progress">Video Call In Progress…</string>
|
||||
<string name="video_call_with_participant">Video call with %s</string>
|
||||
<string name="audio_call_with_participant">Audio call with %s</string>
|
||||
<string name="active_call_with_duration">Active Call (%s)</string>
|
||||
<string name="return_to_call">Return to call</string>
|
||||
<string name="call_resume_action">Resume</string>
|
||||
@ -758,6 +761,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>
|
||||
@ -3274,12 +3279,25 @@
|
||||
<string name="call_tile_you_started_call">You started a call</string>
|
||||
<string name="call_tile_other_started_call">%1$s started a call</string>
|
||||
<string name="call_tile_in_call">You\'re currently in this call</string>
|
||||
<!-- Pattern can be replaced by the value of string call_tile_call_back -->
|
||||
<string name="call_tile_you_declined">You declined this call %1$s</string>
|
||||
<string name="call_tile_you_declined">You declined this call %s</string>
|
||||
<string name="call_tile_you_declined_this_call">You declined this call</string>
|
||||
<string name="call_tile_other_declined">%1$s declined this call</string>
|
||||
<string name="call_tile_ended">This call has ended</string>
|
||||
<string name="call_tile_call_back">Call back</string>
|
||||
|
||||
<string name="call_tile_voice_incoming">Incoming voice call</string>
|
||||
<string name="call_tile_video_incoming">Incoming video call</string>
|
||||
<string name="call_tile_voice_active">Active voice call</string>
|
||||
<string name="call_tile_video_active">Active video call</string>
|
||||
<string name="call_tile_voice_call_has_ended">Voice call ended • %1$s</string>
|
||||
<string name="call_tile_video_call_has_ended">Video call ended • %1$s</string>
|
||||
<string name="call_tile_voice_declined">Voice call declined</string>
|
||||
<string name="call_tile_video_declined">Video call declined</string>
|
||||
<string name="call_tile_voice_missed">Missed voice call</string>
|
||||
<string name="call_tile_video_missed">Missed video call</string>
|
||||
<string name="call_tile_no_answer">No answer</string>
|
||||
<string name="call_tile_connection_failed">Connection failed</string>
|
||||
|
||||
<string name="call_dial_pad_title">Dial pad</string>
|
||||
<string name="call_dial_pad_lookup_error">"There was an error looking up the phone number"</string>
|
||||
|
||||
@ -3293,6 +3311,12 @@
|
||||
<item quantity="one">1 active call (%1$s) · 1 paused call</item>
|
||||
<item quantity="other">1 active call (%1$s) · %2$d paused calls</item>
|
||||
</plurals>
|
||||
<plurals name="call_active_status">
|
||||
<item quantity="one">Active call ·</item>
|
||||
<item quantity="other">%1$d active calls ·</item>
|
||||
</plurals>
|
||||
<string name="call_one_active">Active call (%1$s) ·</string>
|
||||
<string name="call_tap_to_return">%1$s Tap to return</string>
|
||||
|
||||
<string name="call_transfer_consult_first">Consult first</string>
|
||||
<string name="call_transfer_connect_action">Connect</string>
|
||||
@ -3303,6 +3327,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</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>
|
||||
@ -3487,6 +3513,7 @@
|
||||
<string name="room_upgrade_to_recommended_version">Upgrade to the recommended room version</string>
|
||||
|
||||
<string name="error_failed_to_join_room">Sorry, an error occurred while trying to join: %s</string>
|
||||
<string name="call_jitsi_started">Group call started</string>
|
||||
|
||||
<string name="a11y_start_voice_message">Start Voice Message</string>
|
||||
<string name="voice_message_slide_to_cancel">Slide to cancel</string>
|
||||
|
Loading…
Reference in New Issue
Block a user