Merge pull request #3863 from vector-im/feature/fga/new_voip_design

Feature/fga/new voip design
This commit is contained in:
Benoit Marty 2021-08-27 12:02:35 +02:00 committed by GitHub
commit 5b908c404e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
96 changed files with 2281 additions and 1458 deletions

View File

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

@ -0,0 +1 @@
New call designs

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,110 +0,0 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.ui.views
import android.content.Context
import android.text.SpannableString
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.util.AttributeSet
import android.view.View
import android.widget.RelativeLayout
import androidx.core.view.isVisible
import im.vector.app.R
import im.vector.app.core.utils.tappableMatchingText
import im.vector.app.databinding.ViewActiveConferenceViewBinding
import im.vector.app.features.home.room.detail.RoomDetailViewState
import im.vector.app.features.themes.ThemeUtils
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
class ActiveConferenceView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) {
interface Callback {
fun onTapJoinAudio(jitsiWidget: Widget)
fun onTapJoinVideo(jitsiWidget: Widget)
fun onDelete(jitsiWidget: Widget)
}
var callback: Callback? = null
private var jitsiWidget: Widget? = null
private lateinit var views: ViewActiveConferenceViewBinding
init {
setupView()
}
private fun setupView() {
inflate(context, R.layout.view_active_conference_view, this)
views = ViewActiveConferenceViewBinding.bind(this)
setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorPrimary))
// "voice" and "video" texts are underlined and clickable
val voiceString = context.getString(R.string.ongoing_conference_call_voice)
val videoString = context.getString(R.string.ongoing_conference_call_video)
val fullMessage = context.getString(R.string.ongoing_conference_call, voiceString, videoString)
val styledText = SpannableString(fullMessage)
styledText.tappableMatchingText(voiceString, object : ClickableSpan() {
override fun onClick(widget: View) {
jitsiWidget?.let {
callback?.onTapJoinAudio(it)
}
}
})
styledText.tappableMatchingText(videoString, object : ClickableSpan() {
override fun onClick(widget: View) {
jitsiWidget?.let {
callback?.onTapJoinVideo(it)
}
}
})
views.activeConferenceInfo.apply {
text = styledText
movementMethod = LinkMovementMethod.getInstance()
}
views.deleteWidgetButton.setOnClickListener {
jitsiWidget?.let { callback?.onDelete(it) }
}
}
fun render(state: RoomDetailViewState) {
val summary = state.asyncRoomSummary()
if (summary?.membership == Membership.JOIN) {
// We only display banner for 'live' widgets
jitsiWidget = state.activeRoomWidgets()?.firstOrNull {
// for now only jitsi?
it.type == WidgetType.Jitsi
}
isVisible = jitsiWidget != null
// if sent by me or if i can moderate?
views.deleteWidgetButton.isVisible = state.isAllowedToManageWidgets
} else {
isVisible = false
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
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>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
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>

View File

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

View File

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

View File

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

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

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

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

View File

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

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorPrimary"
tools:parentTag="android.widget.RelativeLayout">
<TextView
android:id="@+id/activeConferenceInfo"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toStartOf="@id/deleteWidgetButton"
android:drawablePadding="10dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingTop="12dp"
android:paddingEnd="16dp"
android:paddingBottom="12dp"
android:textColor="?colorOnPrimary"
android:textColorLink="?colorOnPrimary"
app:drawableStartCompat="@drawable/ic_call_answer"
app:drawableTint="?colorOnPrimary"
tools:text="@string/ongoing_conference_call" />
<Button
android:id="@+id/deleteWidgetButton"
style="@style/Widget.Vector.Button.Text.OnPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/activeConferenceInfo"
android:layout_alignBottom="@+id/activeConferenceInfo"
android:layout_alignParentEnd="true"
android:clickable="false"
android:focusable="false"
android:paddingStart="8dp"
android:paddingEnd="16dp"
android:text="@string/action_close"
android:textStyle="bold" />
</merge>

View File

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

View File

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

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

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

View File

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

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

View File

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