Merge branch 'develop' into feature/fga/fix_jitsi_widget

This commit is contained in:
ganfra 2021-08-27 14:22:48 +02:00
commit afa2f13dda
121 changed files with 2322 additions and 1496 deletions

View File

@ -44,6 +44,8 @@ allprojects {
includeGroupByRegex 'com\\.github\\.chrisbanes' includeGroupByRegex 'com\\.github\\.chrisbanes'
// PFLockScreen-Android // PFLockScreen-Android
includeGroupByRegex 'com\\.github\\.vector-im' includeGroupByRegex 'com\\.github\\.vector-im'
// DraggableView
includeGroupByRegex 'com\\.github\\.hyuwah'
// Chat effects // Chat effects
includeGroupByRegex 'com\\.github\\.jetradarmobile' includeGroupByRegex 'com\\.github\\.jetradarmobile'

1
changelog.d/3599.feature Normal file
View File

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

1
changelog.d/3754.feature Normal file
View File

@ -0,0 +1 @@
M11.12 Spaces | Default to 'Home' in settings

View File

@ -28,6 +28,7 @@
<!-- Other useful color --> <!-- Other useful color -->
<!-- Emoji text has to use a black text color --> <!-- Emoji text has to use a black text color -->
<color name="emoji_color">@android:color/black</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> <color name="half_transparent_status_bar">#80000000</color>

View File

@ -28,6 +28,10 @@
<dimen name="pill_min_height">20dp</dimen> <dimen name="pill_min_height">20dp</dimen>
<dimen name="pill_text_padding">4dp</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> <dimen name="item_form_min_height">76dp</dimen>

View File

@ -409,6 +409,7 @@ dependencies {
implementation "androidx.autofill:autofill:$autofill_version" implementation "androidx.autofill:autofill:$autofill_version"
implementation 'jp.wasabeef:glide-transformations:4.3.0' implementation 'jp.wasabeef:glide-transformations:4.3.0'
implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12' 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' implementation 'com.github.Armen101:AudioRecordView:1.0.5'
// Custom Tab // Custom Tab

View File

@ -271,7 +271,11 @@
android:name=".features.attachments.preview.AttachmentsPreviewActivity" android:name=".features.attachments.preview.AttachmentsPreviewActivity"
android:theme="@style/Theme.Vector.Black.AttachmentsPreview" /> android:theme="@style/Theme.Vector.Black.AttachmentsPreview" />
<activity <activity
android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:name=".features.call.VectorCallActivity" android:name=".features.call.VectorCallActivity"
android:launchMode="singleTask"
android:taskAffinity=".features.call.VectorCallActivity"
android:excludeFromRecents="true" /> android:excludeFromRecents="true" />
<!-- PIP Support https://developer.android.com/guide/topics/ui/picture-in-picture --> <!-- PIP Support https://developer.android.com/guide/topics/ui/picture-in-picture -->
<activity <activity

View File

@ -194,6 +194,37 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
</pre> </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> <ul>
<li> <li>
<b>com.github.piasy:BigImageViewer</b> <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.pushers.PushersManager
import im.vector.app.core.utils.AssetReader import im.vector.app.core.utils.AssetReader
import im.vector.app.core.utils.DimensionConverter 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.call.webrtc.WebRtcCallManager
import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.configuration.VectorConfiguration
import im.vector.app.features.crypto.keysrequest.KeyRequestHandler 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.CurrentSpaceSuggestedRoomListDataSource
import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore 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.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.EventHtmlRenderer
import im.vector.app.features.html.VectorHtmlCompressor import im.vector.app.features.html.VectorHtmlCompressor
import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.invite.AutoAcceptInvites
@ -165,7 +165,7 @@ interface VectorComponent {
fun webRtcCallManager(): WebRtcCallManager fun webRtcCallManager(): WebRtcCallManager
fun roomSummaryHolder(): RoomSummariesHolder fun jitsiActiveConferenceHolder(): JitsiActiveConferenceHolder
@Component.Factory @Component.Factory
interface Factory { interface Factory {

View File

@ -98,13 +98,10 @@ class CallRingPlayerOutgoing(
private var player: MediaPlayer? = null private var player: MediaPlayer? = null
fun start() { fun start() {
val audioManager: AudioManager? = applicationContext.getSystemService() applicationContext.getSystemService<AudioManager>()?.mode = AudioManager.MODE_IN_COMMUNICATION
player?.release() player?.release()
player = createPlayer() player = createPlayer()
if (player != null) {
// Check if sound is enabled
val ringerMode = audioManager?.ringerMode
if (player != null && ringerMode == AudioManager.RINGER_MODE_NORMAL) {
try { try {
if (player?.isPlaying == false) { if (player?.isPlaying == false) {
player?.start() player?.start()
@ -116,8 +113,6 @@ class CallRingPlayerOutgoing(
Timber.e(failure, "## VOIP Failed to start ringing outgoing") Timber.e(failure, "## VOIP Failed to start ringing outgoing")
player = null 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.content.Context
import android.util.AttributeSet 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.R
import im.vector.app.databinding.ViewCurrentCallsBinding import im.vector.app.databinding.ViewCurrentCallsBinding
import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.call.webrtc.WebRtcCall
@ -29,7 +31,7 @@ class CurrentCallsView @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0 defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) { ) : FrameLayout(context, attrs, defStyleAttr) {
interface Callback { interface Callback {
fun onTapToReturnToCall() fun onTapToReturnToCall()
@ -42,25 +44,33 @@ class CurrentCallsView @JvmOverloads constructor(
inflate(context, R.layout.view_current_calls, this) inflate(context, R.layout.view_current_calls, this)
views = ViewCurrentCallsBinding.bind(this) views = ViewCurrentCallsBinding.bind(this)
setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorPrimary)) 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() } setOnClickListener { callback?.onTapToReturnToCall() }
} }
fun render(calls: List<WebRtcCall>, formattedDuration: String) { fun render(calls: List<WebRtcCall>, formattedDuration: String) {
val connectedCalls = calls.filter { val tapToReturnFormat = if (calls.size == 1) {
it.mxCall.state is CallState.Connected 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)
} }
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 { } else {
if (heldCalls.isEmpty()) { resources.getQuantityString(R.plurals.call_active_status, calls.size, calls.size)
resources.getString(R.string.call_only_active, formattedDuration)
} else {
resources.getQuantityString(R.plurals.call_one_active_and_other_paused, heldCalls.size, formattedDuration, heldCalls.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.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.databinding.BottomSheetCallControlsBinding import im.vector.app.databinding.BottomSheetCallControlsBinding
import im.vector.app.features.call.audio.CallAudioManager
import me.gujun.android.span.span
class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetCallControlsBinding>() { class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetCallControlsBinding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetCallControlsBinding { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetCallControlsBinding {
@ -45,10 +41,6 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC
renderState(it) renderState(it)
} }
views.callControlsSoundDevice.views.bottomSheetActionClickableZone.debouncedClicks {
callViewModel.handle(VectorCallViewActions.SwitchSoundDevice)
}
views.callControlsSwitchCamera.views.bottomSheetActionClickableZone.debouncedClicks { views.callControlsSwitchCamera.views.bottomSheetActionClickableZone.debouncedClicks {
callViewModel.handle(VectorCallViewActions.ToggleCamera) callViewModel.handle(VectorCallViewActions.ToggleCamera)
dismiss() dismiss()
@ -72,74 +64,11 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC
callViewModel.handle(VectorCallViewActions.InitiateCallTransfer) callViewModel.handle(VectorCallViewActions.InitiateCallTransfer)
dismiss() 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) { 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.isVisible = state.isVideoCall && state.canSwitchCamera
views.callControlsSwitchCamera.subTitle = getString(if (state.isFrontCamera) R.string.call_camera_front else R.string.call_camera_back) views.callControlsSwitchCamera.subTitle = getString(if (state.isFrontCamera) R.string.call_camera_front else R.string.call_camera_back)
if (state.isVideoCall) { if (state.isVideoCall) {
views.callControlsToggleSDHD.isVisible = true views.callControlsToggleSDHD.isVisible = true
if (state.isHD) { if (state.isHD) {

View File

@ -36,16 +36,19 @@ class CallControlsView @JvmOverloads constructor(
init { init {
inflate(context, R.layout.view_call_controls, this) inflate(context, R.layout.view_call_controls, this)
views = ViewCallControlsBinding.bind(this) views = ViewCallControlsBinding.bind(this)
views.audioSettingsIcon.setOnClickListener { didTapAudioSettings() }
views.ringingControlAccept.setOnClickListener { acceptIncomingCall() } views.ringingControlAccept.setOnClickListener { acceptIncomingCall() }
views.ringingControlDecline.setOnClickListener { declineIncomingCall() } views.ringingControlDecline.setOnClickListener { declineIncomingCall() }
views.endCallIcon.setOnClickListener { endOngoingCall() } views.endCallIcon.setOnClickListener { endOngoingCall() }
views.muteIcon.setOnClickListener { toggleMute() } views.muteIcon.setOnClickListener { toggleMute() }
views.videoToggleIcon.setOnClickListener { toggleVideo() } views.videoToggleIcon.setOnClickListener { toggleVideo() }
views.openChatIcon.setOnClickListener { returnToChat() }
views.moreIcon.setOnClickListener { moreControlOption() } views.moreIcon.setOnClickListener { moreControlOption() }
} }
private fun didTapAudioSettings() {
interactionListener?.didTapAudioSettings()
}
private fun acceptIncomingCall() { private fun acceptIncomingCall() {
interactionListener?.didAcceptIncomingCall() interactionListener?.didAcceptIncomingCall()
} }
@ -66,10 +69,6 @@ class CallControlsView @JvmOverloads constructor(
interactionListener?.didTapToggleVideo() interactionListener?.didTapToggleVideo()
} }
private fun returnToChat() {
interactionListener?.returnToChat()
}
private fun moreControlOption() { private fun moreControlOption() {
interactionListener?.didTapMore() interactionListener?.didTapMore()
} }
@ -77,10 +76,10 @@ class CallControlsView @JvmOverloads constructor(
fun updateForState(state: VectorCallViewState) { fun updateForState(state: VectorCallViewState) {
val callState = state.callState.invoke() val callState = state.callState.invoke()
if (state.isAudioMuted) { 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) views.muteIcon.contentDescription = resources.getString(R.string.a11y_unmute_microphone)
} else { } 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) views.muteIcon.contentDescription = resources.getString(R.string.a11y_mute_microphone)
} }
if (state.isVideoEnabled) { if (state.isVideoEnabled) {
@ -92,34 +91,21 @@ class CallControlsView @JvmOverloads constructor(
} }
when (callState) { 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 -> { is CallState.LocalRinging -> {
views.ringingControls.isVisible = true views.ringingControls.isVisible = true
views.ringingControlAccept.isVisible = true views.ringingControlAccept.isVisible = true
views.ringingControlDecline.isVisible = true views.ringingControlDecline.isVisible = true
views.connectedControls.isVisible = false views.connectedControls.isVisible = false
} }
is CallState.Connected -> { is CallState.Connected,
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { is CallState.Dialing,
is CallState.Answering -> {
views.ringingControls.isVisible = false views.ringingControls.isVisible = false
views.connectedControls.isVisible = true views.connectedControls.isVisible = true
views.videoToggleIcon.isVisible = state.isVideoCall views.videoToggleIcon.isVisible = state.isVideoCall
} else { views.moreIcon.isVisible = callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED
views.ringingControls.isVisible = true
views.ringingControlAccept.isVisible = false
views.ringingControlDecline.isVisible = true
views.connectedControls.isVisible = false
} }
} else -> {
is CallState.Ended,
null -> {
views.ringingControls.isVisible = false views.ringingControls.isVisible = false
views.connectedControls.isVisible = false views.connectedControls.isVisible = false
} }
@ -127,12 +113,12 @@ class CallControlsView @JvmOverloads constructor(
} }
interface InteractionListener { interface InteractionListener {
fun didTapAudioSettings()
fun didAcceptIncomingCall() fun didAcceptIncomingCall()
fun didDeclineIncomingCall() fun didDeclineIncomingCall()
fun didEndCall() fun didEndCall()
fun didTapToggleMute() fun didTapToggleMute()
fun didTapToggleVideo() fun didTapToggleVideo()
fun returnToChat()
fun didTapMore() 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 { val callListener = object : WebRtcCall.Listener {
override fun onStateUpdate(call: MxCall) { override fun onStateUpdate(call: MxCall) {
// post it-self liveKnownCalls.postValue(callManager.getCalls())
liveKnownCalls.postValue(liveKnownCalls.value)
} }
override fun onHoldUnhold() { override fun onHoldUnhold() {
super.onHoldUnhold() super.onHoldUnhold()
// post it-self liveKnownCalls.postValue(callManager.getCalls())
liveKnownCalls.postValue(liveKnownCalls.value)
} }
} }

View File

@ -17,13 +17,17 @@
package im.vector.app.features.call package im.vector.app.features.call
import android.app.KeyguardManager import android.app.KeyguardManager
import android.app.PictureInPictureParams
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
import android.content.res.Configuration
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.util.Rational
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import androidx.annotation.StringRes import androidx.annotation.StringRes
@ -35,9 +39,11 @@ import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.viewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.card.MaterialCardView
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.ScreenComponent 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.platform.VectorBaseActivity
import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL
import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_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.AvatarRenderer
import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailActivity
import im.vector.app.features.home.room.detail.RoomDetailArgs 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 io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
@ -87,7 +95,6 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
} }
private val callViewModel: VectorCallViewModel by viewModel() private val callViewModel: VectorCallViewModel by viewModel()
private lateinit var callArgs: CallArgs
@Inject lateinit var callManager: WebRtcCallManager @Inject lateinit var callManager: WebRtcCallManager
@Inject lateinit var viewModelFactory: VectorCallViewModel.Factory @Inject lateinit var viewModelFactory: VectorCallViewModel.Factory
@ -99,6 +106,8 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
} }
private var rootEglBase: EglBase? = null private var rootEglBase: EglBase? = null
private var pipDraggrableView: DraggableView<MaterialCardView>? = null
private var otherCallDraggableView: DraggableView<MaterialCardView>? = null
var surfaceRenderersAreInitialized = false var surfaceRenderersAreInitialized = false
@ -115,13 +124,6 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
window.navigationBarColor = Color.BLACK window.navigationBarColor = Color.BLACK
super.onCreate(savedInstanceState) 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)}") Timber.tag(loggerTag.value).v("EXTRA_MODE is ${intent.getStringExtra(EXTRA_MODE)}")
if (intent.getStringExtra(EXTRA_MODE) == INCOMING_RINGING) { if (intent.getStringExtra(EXTRA_MODE) == INCOMING_RINGING) {
turnScreenOnAndKeyguardOff() turnScreenOnAndKeyguardOff()
@ -129,6 +131,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
if (savedInstanceState != null) { if (savedInstanceState != null) {
(supportFragmentManager.findFragmentByTag(FRAGMENT_DIAL_PAD_TAG) as? CallDialPadBottomSheet)?.callback = dialPadCallback (supportFragmentManager.findFragmentByTag(FRAGMENT_DIAL_PAD_TAG) as? CallDialPadBottomSheet)?.callback = dialPadCallback
} }
setSupportActionBar(views.callToolbar)
configureCallViews() configureCallViews()
callViewModel.subscribe(this) { callViewModel.subscribe(this) {
@ -149,25 +152,89 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
} }
.disposeOnDestroy() .disposeOnDestroy()
if (callArgs.isVideoCall) { 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)) { if (checkPermissions(PERMISSIONS_FOR_VIDEO_IP_CALL, this, permissionCameraLauncher, R.string.permissions_rationale_msg_camera_and_audio)) {
start() setupRenderersIfNeeded()
} }
} else { } else {
if (checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL, this, permissionCameraLauncher, R.string.permissions_rationale_msg_record_audio)) { if (checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL, this, permissionCameraLauncher, R.string.permissions_rationale_msg_record_audio)) {
start() 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() { 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) { if (surfaceRenderersAreInitialized) {
views.pipRenderer.release() views.pipRenderer.release()
views.fullscreenRenderer.release() views.fullscreenRenderer.release()
surfaceRenderersAreInitialized = false
} }
turnScreenOffAndKeyguardOn()
super.onDestroy()
} }
private fun renderState(state: VectorCallViewState) { private fun renderState(state: VectorCallViewState) {
@ -176,53 +243,57 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
finish() finish()
return 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) views.callControlsView.updateForState(state)
val callState = state.callState.invoke() val callState = state.callState.invoke()
views.callConnectingProgress.isVisible = false
views.callActionText.setOnClickListener(null) views.callActionText.setOnClickListener(null)
views.callActionText.isVisible = false views.callActionText.isVisible = false
views.smallIsHeldIcon.isVisible = false views.smallIsHeldIcon.isVisible = false
when (callState) { when (callState) {
is CallState.Idle, is CallState.Idle,
is CallState.CreateOffer, is CallState.CreateOffer,
is CallState.LocalRinging,
is CallState.Dialing -> { is CallState.Dialing -> {
views.callVideoGroup.isInvisible = true views.fullscreenRenderer.isVisible = false
views.pipRendererWrapper.isVisible = false
views.callInfoGroup.isVisible = true views.callInfoGroup.isVisible = true
views.callStatusText.setText(R.string.call_ring) views.callToolbar.setSubtitle(R.string.call_ringing)
configureCallInfo(state) configureCallInfo(state)
} }
is CallState.LocalRinging -> {
views.callVideoGroup.isInvisible = true
views.callInfoGroup.isVisible = true
views.callStatusText.text = null
configureCallInfo(state)
}
is CallState.Answering -> { is CallState.Answering -> {
views.callVideoGroup.isInvisible = true views.fullscreenRenderer.isVisible = false
views.pipRendererWrapper.isVisible = false
views.callInfoGroup.isVisible = true views.callInfoGroup.isVisible = true
views.callStatusText.setText(R.string.call_connecting) views.callToolbar.setSubtitle(R.string.call_connecting)
views.callConnectingProgress.isVisible = true
configureCallInfo(state) configureCallInfo(state)
} }
is CallState.Connected -> { is CallState.Connected -> {
views.callToolbar.subtitle = state.formattedDuration
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
if (state.isLocalOnHold || state.isRemoteOnHold) { if (state.isLocalOnHold || state.isRemoteOnHold) {
views.smallIsHeldIcon.isVisible = true views.smallIsHeldIcon.isVisible = true
views.callVideoGroup.isInvisible = true views.fullscreenRenderer.isVisible = false
views.pipRendererWrapper.isVisible = false
views.callInfoGroup.isVisible = true views.callInfoGroup.isVisible = true
configureCallInfo(state, blurAvatar = true) configureCallInfo(state, blurAvatar = true)
if (state.isRemoteOnHold) { if (state.isRemoteOnHold) {
views.callActionText.setText(R.string.call_resume_action) views.callActionText.setText(R.string.call_resume_action)
views.callActionText.isVisible = true views.callActionText.isVisible = true
views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.ToggleHoldResume) } 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 { } else {
views.callActionText.isInvisible = true views.callActionText.isInvisible = true
state.callInfo?.opponentUserItem?.let { 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) { } 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.text = getString(R.string.call_transfer_transfer_to_title, transfereeName)
views.callActionText.isVisible = true views.callActionText.isVisible = true
views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.TransferCall) } views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.TransferCall) }
views.callStatusText.text = state.formattedDuration
configureCallInfo(state) configureCallInfo(state)
} else { } else {
views.callStatusText.text = state.formattedDuration
configureCallInfo(state) configureCallInfo(state)
if (callArgs.isVideoCall) { if (state.isVideoCall) {
views.callVideoGroup.isVisible = true views.fullscreenRenderer.isVisible = true
views.pipRendererWrapper.isVisible = true
views.callInfoGroup.isVisible = false views.callInfoGroup.isVisible = false
views.pipRenderer.isVisible = !state.isVideoCaptureInError && state.otherKnownCallInfo == null views.pipRenderer.isVisible = !state.isVideoCaptureInError && state.otherKnownCallInfo == null
} else { } else {
views.callVideoGroup.isInvisible = true views.fullscreenRenderer.isVisible = false
views.pipRendererWrapper.isVisible = false
views.callInfoGroup.isVisible = true views.callInfoGroup.isVisible = true
} }
} }
} else { } else {
// This state is not final, if you change network, new candidates will be sent // 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 views.callInfoGroup.isVisible = true
configureCallInfo(state) configureCallInfo(state)
views.callStatusText.setText(R.string.call_connecting) views.callToolbar.setSubtitle(R.string.call_connecting)
views.callConnectingProgress.isVisible = true
} }
} }
is CallState.Ended -> { is CallState.Ended -> {
views.callVideoGroup.isInvisible = true views.fullscreenRenderer.isVisible = false
views.pipRendererWrapper.isVisible = false
views.callInfoGroup.isVisible = true views.callInfoGroup.isVisible = true
views.callStatusText.setText(R.string.call_ended) views.callToolbar.setSubtitle(R.string.call_ended)
configureCallInfo(state) configureCallInfo(state)
} }
else -> { else -> {
views.callVideoGroup.isInvisible = true views.fullscreenRenderer.isVisible = false
views.pipRendererWrapper.isVisible = false
views.callInfoGroup.isInvisible = true 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) { 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) { when (callState.reason) {
EndCallReason.USER_BUSY -> { EndCallReason.USER_BUSY -> {
showEndCallDialog(R.string.call_ended_user_busy_title, R.string.call_ended_user_busy_description) 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) val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen_blur)
avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter, addPlaceholder = false) avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter, addPlaceholder = false)
if (state.transferee is VectorCallViewState.TransfereeState.NoTransferee) { 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 { } else {
views.participantNameText.text = getString(R.string.call_transfer_consulting_with, it.getBestName()) getString(R.string.audio_call_with_participant, it.getBestName())
}
} else {
views.participantNameText.setTextOrHide(getString(R.string.call_transfer_consulting_with, it.getBestName()))
} }
if (blurAvatar) { if (blurAvatar) {
avatarRenderer.renderBlur(it, views.otherMemberAvatar, sampling = 2, rounded = true, colorFilter = colorFilter, addPlaceholder = true) 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) avatarRenderer.render(it, views.otherMemberAvatar)
} }
} }
if (state.otherKnownCallInfo?.opponentUserItem == null) { if (state.otherKnownCallInfo?.opponentUserItem == null || isInPictureInPictureModeSafe()) {
views.otherKnownCallLayout.isVisible = false views.otherKnownCallLayout.isVisible = false
} else { } else {
val otherCall = callManager.getCallById(state.otherKnownCallInfo.callId) val otherCall = callManager.getCallById(state.otherKnownCallInfo.callId)
@ -324,7 +447,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
addPlaceholder = true addPlaceholder = true
) )
views.otherKnownCallLayout.isVisible = 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 { views.otherKnownCallLayout.setOnClickListener {
withState(callViewModel) { withState(callViewModel) {
val otherCall = callManager.getCallById(it.otherKnownCallInfo?.callId ?: "") ?: return@withState val otherCall = callManager.getCallById(it.otherKnownCallInfo?.callId ?: "") ?: return@withState
startActivity(newIntent(this, otherCall, null)) val callArgs = CallArgs(
finish() 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, _ -> private val permissionCameraLauncher = registerForPermissionsResult { allGranted, _ ->
if (allGranted) { if (allGranted) {
start() setupRenderersIfNeeded()
} else { } else {
// TODO display something // TODO display something
finish() finish()
} }
} }
private fun start() { private fun setupRenderersIfNeeded() {
detachRenderersIfNeeded()
rootEglBase = EglUtils.rootEglBase ?: return Unit.also { rootEglBase = EglUtils.rootEglBase ?: return Unit.also {
Timber.tag(loggerTag.value).v("rootEglBase is null") Timber.tag(loggerTag.value).v("rootEglBase is null")
finish() finish()
} }
// Init Picture in Picture renderer // Init Picture in Picture renderer
views.pipRenderer.init(rootEglBase!!.eglBaseContext, null) views.pipRenderer.apply {
views.pipRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) init(rootEglBase!!.eglBaseContext, null)
setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_BALANCED)
setEnableHardwareScaler(true)
setZOrderMediaOverlay(true)
}
// Init Full Screen renderer // Init Full Screen renderer
views.fullscreenRenderer.init(rootEglBase!!.eglBaseContext, null) views.fullscreenRenderer.init(rootEglBase!!.eglBaseContext, null)
views.fullscreenRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL) views.fullscreenRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL)
views.pipRenderer.setZOrderMediaOverlay(true)
views.pipRenderer.setEnableHardwareScaler(true /* enabled */)
views.fullscreenRenderer.setEnableHardwareScaler(true /* enabled */) views.fullscreenRenderer.setEnableHardwareScaler(true /* enabled */)
callManager.getCallById(callArgs.callId)?.attachViewRenderers(views.pipRenderer, views.fullscreenRenderer, val callId = withState(callViewModel) { it.callId }
intent.getStringExtra(EXTRA_MODE)?.takeIf { isFirstCreation() }) callManager.getCallById(callId)?.also { webRtcCall ->
webRtcCall.attachViewRenderers(views.pipRenderer, views.fullscreenRenderer, intent.getStringExtra(EXTRA_MODE))
views.pipRenderer.setOnClickListener { intent.removeExtra(EXTRA_MODE)
callViewModel.handle(VectorCallViewActions.ToggleCamera)
} }
surfaceRenderersAreInitialized = true surfaceRenderersAreInitialized = true
} }
@ -387,7 +526,8 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
}.show(supportFragmentManager, FRAGMENT_DIAL_PAD_TAG) }.show(supportFragmentManager, FRAGMENT_DIAL_PAD_TAG)
} }
is VectorCallViewEvents.ShowCallTransferScreen -> { is VectorCallViewEvents.ShowCallTransferScreen -> {
navigator.openCallTransfer(this, callArgs.callId) val callId = withState(callViewModel) { it.callId }
navigator.openCallTransfer(this, callId)
} }
null -> { null -> {
} }
@ -406,37 +546,8 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
.show() .show()
} }
companion object { override fun didTapAudioSettings() {
private const val EXTRA_MODE = "EXTRA_MODE" CallSoundDeviceChooserBottomSheet().show(supportFragmentManager, "SoundDeviceChooser")
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 didAcceptIncomingCall() { override fun didAcceptIncomingCall() {
@ -459,8 +570,9 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
callViewModel.handle(VectorCallViewActions.ToggleVideo) callViewModel.handle(VectorCallViewActions.ToggleVideo)
} }
override fun returnToChat() { private fun returnToChat() {
val args = RoomDetailArgs(callArgs.signalingRoomId) val roomId = withState(callViewModel) { it.roomId }
val args = RoomDetailArgs(roomId)
val intent = RoomDetailActivity.newIntent(this, args).apply { val intent = RoomDetailActivity.newIntent(this, args).apply {
flags = FLAG_ACTIVITY_CLEAR_TOP 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() data class ChangeAudioDevice(val device: CallAudioManager.Device) : VectorCallViewActions()
object OpenDialPad: VectorCallViewActions() object OpenDialPad: VectorCallViewActions()
data class SendDtmfDigit(val digit: String) : VectorCallViewActions() data class SendDtmfDigit(val digit: String) : VectorCallViewActions()
data class SwitchCall(val callArgs: CallArgs) : VectorCallViewActions()
object SwitchSoundDevice : VectorCallViewActions() object SwitchSoundDevice : VectorCallViewActions()
object HeadSetButtonPressed : VectorCallViewActions() object HeadSetButtonPressed : VectorCallViewActions()
object ToggleCamera : VectorCallViewActions() object ToggleCamera : VectorCallViewActions()

View File

@ -60,7 +60,7 @@ class VectorCallViewModel @AssistedInject constructor(
setState { setState {
copy( copy(
isLocalOnHold = call?.isLocalOnHold ?: false, isLocalOnHold = call?.isLocalOnHold ?: false,
isRemoteOnHold = call?.remoteOnHold ?: false isRemoteOnHold = call?.isRemoteOnHold ?: false
) )
} }
} }
@ -144,7 +144,7 @@ class VectorCallViewModel @AssistedInject constructor(
override fun onAudioDevicesChange() { override fun onAudioDevicesChange() {
val currentSoundDevice = callManager.audioManager.selectedDevice ?: return val currentSoundDevice = callManager.audioManager.selectedDevice ?: return
if (currentSoundDevice == CallAudioManager.Device.PHONE) { if (currentSoundDevice == CallAudioManager.Device.Phone) {
proximityManager.start() proximityManager.start()
} else { } else {
proximityManager.stop() proximityManager.stop()
@ -172,7 +172,12 @@ class VectorCallViewModel @AssistedInject constructor(
} }
init { 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) { if (webRtcCall == null) {
setState { setState {
copy(callState = Fail(IllegalArgumentException("No call"))) copy(callState = Fail(IllegalArgumentException("No call")))
@ -182,17 +187,19 @@ class VectorCallViewModel @AssistedInject constructor(
callManager.addCurrentCallListener(currentCallListener) callManager.addCurrentCallListener(currentCallListener)
webRtcCall.addListener(callListener) webRtcCall.addListener(callListener)
val currentSoundDevice = callManager.audioManager.selectedDevice val currentSoundDevice = callManager.audioManager.selectedDevice
if (currentSoundDevice == CallAudioManager.Device.PHONE) { if (currentSoundDevice == CallAudioManager.Device.Phone) {
proximityManager.start() proximityManager.start()
} }
setState { setState {
copy( copy(
isAudioMuted = webRtcCall.micMuted,
isVideoEnabled = !webRtcCall.videoMuted,
isVideoCall = webRtcCall.mxCall.isVideoCall, isVideoCall = webRtcCall.mxCall.isVideoCall,
callState = Success(webRtcCall.mxCall.state), callState = Success(webRtcCall.mxCall.state),
callInfo = webRtcCall.extractCallInfo(), callInfo = webRtcCall.extractCallInfo(),
device = currentSoundDevice ?: CallAudioManager.Device.PHONE, device = currentSoundDevice ?: CallAudioManager.Device.Phone,
isLocalOnHold = webRtcCall.isLocalOnHold, isLocalOnHold = webRtcCall.isLocalOnHold,
isRemoteOnHold = webRtcCall.remoteOnHold, isRemoteOnHold = webRtcCall.isRemoteOnHold,
availableDevices = callManager.audioManager.availableDevices, availableDevices = callManager.audioManager.availableDevices,
isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT, isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT,
canSwitchCamera = webRtcCall.canSwitchCamera(), canSwitchCamera = webRtcCall.canSwitchCamera(),
@ -225,6 +232,7 @@ class VectorCallViewModel @AssistedInject constructor(
override fun onCleared() { override fun onCleared() {
callManager.removeCurrentCallListener(currentCallListener) callManager.removeCurrentCallListener(currentCallListener)
call?.removeListener(callListener) call?.removeListener(callListener)
call = null
proximityManager.stop() proximityManager.stop()
super.onCleared() super.onCleared()
} }
@ -305,6 +313,10 @@ class VectorCallViewModel @AssistedInject constructor(
VectorCallViewActions.TransferCall -> { VectorCallViewActions.TransferCall -> {
handleCallTransfer() handleCallTransfer()
} }
is VectorCallViewActions.SwitchCall -> {
setState { VectorCallViewState(action.callArgs) }
setupCallWithCurrentState()
}
}.exhaustive }.exhaustive
} }

View File

@ -35,7 +35,7 @@ data class VectorCallViewState(
val isHD: Boolean = false, val isHD: Boolean = false,
val isFrontCamera: Boolean = true, val isFrontCamera: Boolean = true,
val canSwitchCamera: 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 availableDevices: Set<CallAudioManager.Device> = emptySet(),
val callState: Async<CallState> = Uninitialized, val callState: Async<CallState> = Uninitialized,
val otherKnownCallInfo: CallInfo? = null, val otherKnownCallInfo: CallInfo? = null,

View File

@ -50,13 +50,17 @@ internal class API21AudioDeviceDetector(private val context: Context,
private fun getAvailableSoundDevices(): Set<CallAudioManager.Device> { private fun getAvailableSoundDevices(): Set<CallAudioManager.Device> {
return HashSet<CallAudioManager.Device>().apply { return HashSet<CallAudioManager.Device>().apply {
if (isBluetoothHeadsetOn()) add(CallAudioManager.Device.WIRELESS_HEADSET) if (isBluetoothHeadsetOn()) {
if (isWiredHeadsetOn()) { connectedBlueToothHeadset?.connectedDevices?.forEach {
add(CallAudioManager.Device.HEADSET) add(CallAudioManager.Device.WirelessHeadset(it.name))
} else {
add(CallAudioManager.Device.PHONE)
} }
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) val deviceInfos = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
for (info in deviceInfos) { for (info in deviceInfos) {
when (info.type) { when (info.type) {
AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> devices.add(CallAudioManager.Device.WIRELESS_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_EARPIECE -> devices.add(CallAudioManager.Device.Phone)
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> devices.add(CallAudioManager.Device.SPEAKER) 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_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET, TYPE_USB_HEADSET -> devices.add(CallAudioManager.Device.Headset)
} }
} }
callAudioManager.replaceDevices(devices) callAudioManager.replaceDevices(devices)

View File

@ -19,7 +19,10 @@ package im.vector.app.features.call.audio
import android.content.Context import android.content.Context
import android.media.AudioManager import android.media.AudioManager
import android.os.Build import android.os.Build
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import im.vector.app.R
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import timber.log.Timber import timber.log.Timber
import java.util.HashSet import java.util.HashSet
@ -31,11 +34,11 @@ class CallAudioManager(private val context: Context, val configChange: (() -> Un
private var audioDeviceDetector: AudioDeviceDetector? = null private var audioDeviceDetector: AudioDeviceDetector? = null
private var audioDeviceRouter: AudioDeviceRouter? = null private var audioDeviceRouter: AudioDeviceRouter? = null
enum class Device { sealed class Device(@StringRes val titleRes: Int, @DrawableRes val drawableRes: Int) {
PHONE, object Phone : Device(R.string.sound_device_phone, R.drawable.ic_sound_device_phone)
SPEAKER, object Speaker : Device(R.string.sound_device_speaker, R.drawable.ic_sound_device_speaker)
HEADSET, object Headset : Device(R.string.sound_device_headset, R.drawable.ic_sound_device_headphone)
WIRELESS_HEADSET data class WirelessHeadset(val name: String?) : Device(R.string.sound_device_wireless_headset, R.drawable.ic_sound_device_wireless)
} }
enum class Mode { enum class Mode {
@ -133,19 +136,19 @@ class CallAudioManager(private val context: Context, val configChange: (() -> Un
userSelectedDevice = null userSelectedDevice = null
return true return true
} }
val bluetoothAvailable = _availableDevices.contains(Device.WIRELESS_HEADSET) val availableBluetoothDevice = _availableDevices.firstOrNull { it is Device.WirelessHeadset }
val headsetAvailable = _availableDevices.contains(Device.HEADSET) val headsetAvailable = _availableDevices.contains(Device.Headset)
// Pick the desired device based on what's available and the mode. // Pick the desired device based on what's available and the mode.
var audioDevice: Device var audioDevice: Device
audioDevice = if (bluetoothAvailable) { audioDevice = if (availableBluetoothDevice != null) {
Device.WIRELESS_HEADSET availableBluetoothDevice
} else if (headsetAvailable) { } else if (headsetAvailable) {
Device.HEADSET Device.Headset
} else if (mode == Mode.VIDEO_CALL) { } else if (mode == Mode.VIDEO_CALL) {
Device.SPEAKER Device.Speaker
} else { } else {
Device.PHONE Device.Phone
} }
// Consider the user's selection // Consider the user's selection
if (userSelectedDevice != null && _availableDevices.contains(userSelectedDevice)) { if (userSelectedDevice != null && _availableDevices.contains(userSelectedDevice)) {

View File

@ -31,8 +31,8 @@ class DefaultAudioDeviceRouter(private val audioManager: AudioManager,
private var focusRequestCompat: AudioFocusRequestCompat? = null private var focusRequestCompat: AudioFocusRequestCompat? = null
override fun setAudioRoute(device: CallAudioManager.Device) { override fun setAudioRoute(device: CallAudioManager.Device) {
audioManager.isSpeakerphoneOn = device === CallAudioManager.Device.SPEAKER audioManager.isSpeakerphoneOn = device is CallAudioManager.Device.Speaker
setBluetoothAudioRoute(device === CallAudioManager.Device.WIRELESS_HEADSET) setBluetoothAudioRoute(device is CallAudioManager.Device.WirelessHeadset)
} }
override fun setMode(mode: CallAudioManager.Mode): Boolean { 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

@ -122,6 +122,12 @@ class JitsiService @Inject constructor(
) )
} }
fun extractJitsiWidgetData(widget: Widget): JitsiWidgetData? {
return tryOrNull {
jitsiWidgetDataFactory.create(widget)
}
}
private fun JitsiWidgetData.isOpenIdJWTAuthenticationRequired(): Boolean { private fun JitsiWidgetData.isOpenIdJWTAuthenticationRequired(): Boolean {
return auth == JITSI_OPEN_ID_TOKEN_JWT_AUTH return auth == 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 package im.vector.app.features.call.conference
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.Toast import android.widget.Toast
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.lifecycle.Lifecycle
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
@ -41,6 +40,7 @@ import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivityJitsiBinding import im.vector.app.databinding.ActivityJitsiBinding
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.jitsi.meet.sdk.BroadcastEvent import org.jitsi.meet.sdk.BroadcastEvent
import org.jitsi.meet.sdk.JitsiMeet
import org.jitsi.meet.sdk.JitsiMeetActivityDelegate import org.jitsi.meet.sdk.JitsiMeetActivityDelegate
import org.jitsi.meet.sdk.JitsiMeetActivityInterface import org.jitsi.meet.sdk.JitsiMeetActivityInterface
import org.jitsi.meet.sdk.JitsiMeetConferenceOptions import org.jitsi.meet.sdk.JitsiMeetConferenceOptions
@ -71,13 +71,6 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
injector.inject(this) 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -94,8 +87,47 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
JitsiCallViewEvents.LeaveConference -> handleLeaveConference() JitsiCallViewEvents.LeaveConference -> handleLeaveConference()
}.exhaustive }.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() { private fun handleLeaveConference() {
@ -116,14 +148,16 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean,
newConfig: Configuration) { newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
checkIfActivityShouldBeFinished()
Timber.w("onPictureInPictureModeChanged($isInPictureInPictureMode)") Timber.w("onPictureInPictureModeChanged($isInPictureInPictureMode)")
} }
override fun initUiAndData() { private fun checkIfActivityShouldBeFinished() {
super.initUiAndData() // OnStop is called when PiP mode is closed directly from the ui
jitsiMeetView = JitsiMeetView(this) // 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.
val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) && !isInPictureInPictureMode) {
views.jitsiLayout.addView(jitsiMeetView, params) finishAndRemoveTask()
}
} }
private fun renderState(viewState: JitsiCallViewState) { private fun renderState(viewState: JitsiCallViewState) {
@ -167,34 +201,6 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
jitsiMeetView?.join(jitsiMeetConferenceOptions) 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?) { override fun onNewIntent(intent: Intent?) {
JitsiMeetActivityDelegate.onNewIntent(intent) JitsiMeetActivityDelegate.onNewIntent(intent)
@ -217,24 +223,7 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
JitsiMeetActivityDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults) JitsiMeetActivityDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults)
} }
private fun registerForBroadcastMessages() { private fun onBroadcastEvent(event: BroadcastEvent) {
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)
Timber.v("Broadcast received: ${event.type}") Timber.v("Broadcast received: ${event.type}")
when (event.type) { when (event.type) {
BroadcastEvent.Type.CONFERENCE_TERMINATED -> onConferenceTerminated(event.data) BroadcastEvent.Type.CONFERENCE_TERMINATED -> onConferenceTerminated(event.data)

View File

@ -175,7 +175,7 @@ class WebRtcCall(
private set private set
var videoMuted = false var videoMuted = false
private set private set
var remoteOnHold = false var isRemoteOnHold = false
private set private set
var isLocalOnHold = false var isLocalOnHold = false
private set private set
@ -357,7 +357,7 @@ class WebRtcCall(
fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) { fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) {
sessionScope?.launch(dispatcher) { sessionScope?.launch(dispatcher) {
Timber.tag(loggerTag.value).v("attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer") Timber.tag(loggerTag.value).v("attachViewRenderers localRenderer $localViewRenderer / $remoteViewRenderer")
localSurfaceRenderers.addIfNeeded(localViewRenderer) localSurfaceRenderers.addIfNeeded(localViewRenderer)
remoteSurfaceRenderers.addIfNeeded(remoteViewRenderer) remoteSurfaceRenderers.addIfNeeded(remoteViewRenderer)
when (mode) { when (mode) {
@ -614,12 +614,12 @@ class WebRtcCall(
} }
private fun updateMuteStatus() { private fun updateMuteStatus() {
val micShouldBeMuted = micMuted || remoteOnHold val micShouldBeMuted = micMuted || isRemoteOnHold
localAudioTrack?.setEnabled(!micShouldBeMuted) localAudioTrack?.setEnabled(!micShouldBeMuted)
remoteAudioTrack?.setEnabled(!remoteOnHold) remoteAudioTrack?.setEnabled(!isRemoteOnHold)
val vidShouldBeMuted = videoMuted || remoteOnHold val vidShouldBeMuted = videoMuted || isRemoteOnHold
localVideoTrack?.setEnabled(!vidShouldBeMuted) localVideoTrack?.setEnabled(!vidShouldBeMuted)
remoteVideoTrack?.setEnabled(!remoteOnHold) remoteVideoTrack?.setEnabled(!isRemoteOnHold)
} }
/** /**
@ -645,16 +645,16 @@ class WebRtcCall(
fun updateRemoteOnHold(onHold: Boolean) { fun updateRemoteOnHold(onHold: Boolean) {
sessionScope?.launch(dispatcher) { sessionScope?.launch(dispatcher) {
if (remoteOnHold == onHold) return@launch if (isRemoteOnHold == onHold) return@launch
val direction: RtpTransceiver.RtpTransceiverDirection val direction: RtpTransceiver.RtpTransceiverDirection
if (onHold) { if (onHold) {
wasLocalOnHold = isLocalOnHold wasLocalOnHold = isLocalOnHold
remoteOnHold = true isRemoteOnHold = true
isLocalOnHold = true isLocalOnHold = true
direction = RtpTransceiver.RtpTransceiverDirection.SEND_ONLY direction = RtpTransceiver.RtpTransceiverDirection.SEND_ONLY
timer.pause() timer.pause()
} else { } else {
remoteOnHold = false isRemoteOnHold = false
isLocalOnHold = wasLocalOnHold isLocalOnHold = wasLocalOnHold
onCallBecomeActive(this@WebRtcCall) onCallBecomeActive(this@WebRtcCall)
direction = RtpTransceiver.RtpTransceiverDirection.SEND_RECV direction = RtpTransceiver.RtpTransceiverDirection.SEND_RECV

View File

@ -16,17 +16,20 @@
package im.vector.app.features.call.webrtc 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.session.Session
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
fun WebRtcCall.getOpponentAsMatrixItem(session: Session): MatrixItem? { 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. // Fallback to RoomSummary if there is no other member.
if (roomSummary.otherMemberIds.isEmpty()) { if (roomSummary.otherMemberIds.isEmpty().orFalse()) {
roomSummary.toMatrixItem() roomSummary.toMatrixItem()
} else { } 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 { override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressed()
return true
}
if (item.itemId == R.id.menuItemEdit) { if (item.itemId == R.id.menuItemEdit) {
viewModel.handle(RoomDevToolAction.MenuEdit) viewModel.handle(RoomDevToolAction.MenuEdit)
return true return true

View File

@ -22,7 +22,6 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.get
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.iterator import androidx.core.view.iterator
import androidx.fragment.app.Fragment 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.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.ui.views.CurrentCallsView 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.KeysBackupBanner
import im.vector.app.core.ui.views.KnownCallsViewHolder
import im.vector.app.databinding.FragmentHomeDetailBinding import im.vector.app.databinding.FragmentHomeDetailBinding
import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.call.SharedKnownCallsViewModel
import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.VectorCallActivity
@ -117,7 +116,7 @@ class HomeDetailFragment @Inject constructor(
return FragmentHomeDetailBinding.inflate(inflater, container, false) return FragmentHomeDetailBinding.inflate(inflater, container, false)
} }
private val activeCallViewHolder = KnownCallsViewHolder() private val currentCallsViewPresenter = CurrentCallsViewPresenter()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -190,11 +189,16 @@ class HomeDetailFragment @Inject constructor(
sharedCallActionViewModel sharedCallActionViewModel
.liveKnownCalls .liveKnownCalls
.observe(viewLifecycleOwner, { .observe(viewLifecycleOwner, {
activeCallViewHolder.updateCall(callManager.getCurrentCall(), callManager.getCalls()) currentCallsViewPresenter.updateCall(callManager.getCurrentCall(), callManager.getCalls())
invalidateOptionsMenu() invalidateOptionsMenu()
}) })
} }
override fun onDestroyView() {
currentCallsViewPresenter.unBind()
super.onDestroyView()
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// update notification tab if needed // update notification tab if needed
@ -291,12 +295,7 @@ class HomeDetailFragment @Inject constructor(
} }
private fun setupActiveCallView() { private fun setupActiveCallView() {
activeCallViewHolder.bind( currentCallsViewPresenter.bind(views.currentCallsView, this)
views.activeCallPiP,
views.activeCallView,
views.activeCallPiPWrap,
this
)
} }
private fun setupToolbar() { private fun setupToolbar() {

View File

@ -147,7 +147,7 @@ class UnreadMessagesSharedViewModel @AssistedInject constructor(@Assisted initia
roomSummaryQueryParams { roomSummaryQueryParams {
this.memberships = listOf(Membership.JOIN) this.memberships = listOf(Membership.JOIN)
this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null).takeIf { this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null).takeIf {
vectorPreferences.labsSpacesOnlyOrphansInHome() !vectorPreferences.prefSpacesShowAllRoomInHome()
} ?: ActiveSpaceFilter.None } ?: ActiveSpaceFilter.None
} }
) )

View File

@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail
import android.net.Uri import android.net.Uri
import android.view.View import android.view.View
import im.vector.app.core.platform.VectorViewModelAction 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.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
@ -89,9 +90,14 @@ sealed class RoomDetailAction : VectorViewModelAction {
object ManageIntegrations : RoomDetailAction() object ManageIntegrations : RoomDetailAction()
data class AddJitsiWidget(val withVideo: Boolean) : RoomDetailAction() data class AddJitsiWidget(val withVideo: Boolean) : RoomDetailAction()
data class RemoveWidget(val widgetId: String) : RoomDetailAction() data class RemoveWidget(val widgetId: String) : RoomDetailAction()
object JoinJitsiCall: RoomDetailAction()
object LeaveJitsiCall: RoomDetailAction()
data class EnsureNativeWidgetAllowed(val widget: Widget, data class EnsureNativeWidgetAllowed(val widget: Widget,
val userJustAccepted: Boolean, val userJustAccepted: Boolean,
val grantedEvents: RoomDetailViewEvents) : RoomDetailAction() val grantedEvents: RoomDetailViewEvents) : RoomDetailAction()
data class UpdateJoinJitsiCallStatus(val jitsiEvent: BroadcastEvent): RoomDetailAction()
data class OpenOrCreateDm(val userId: String) : RoomDetailAction() data class OpenOrCreateDm(val userId: String) : RoomDetailAction()
data class JumpToReadReceipt(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 package im.vector.app.features.home.room.detail
import android.animation.ArgbEvaluator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
@ -65,6 +66,7 @@ import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.jakewharton.rxbinding3.view.focusChanges import com.jakewharton.rxbinding3.view.focusChanges
import com.jakewharton.rxbinding3.widget.textChanges 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.VectorBaseFragment
import im.vector.app.core.platform.showOptimizedSnackbar import im.vector.app.core.platform.showOptimizedSnackbar
import im.vector.app.core.resources.ColorProvider 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.CurrentCallsView
import im.vector.app.core.ui.views.CurrentCallsViewPresenter
import im.vector.app.core.ui.views.FailedMessagesWarningView 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.ui.views.NotificationAreaView
import im.vector.app.core.utils.Debouncer import im.vector.app.core.utils.Debouncer
import im.vector.app.core.utils.DimensionConverter 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.attachments.toGroupedContentAttachmentData
import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.call.SharedKnownCallsViewModel
import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.conference.JitsiBroadcastEmitter
import im.vector.app.features.call.conference.JitsiBroadcastEventObserver
import im.vector.app.features.call.conference.JitsiCallViewModel import im.vector.app.features.call.conference.JitsiCallViewModel
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.command.Command import im.vector.app.features.command.Command
@ -182,6 +185,7 @@ import nl.dionsegijn.konfetti.models.Shape
import nl.dionsegijn.konfetti.models.Size import nl.dionsegijn.konfetti.models.Size
import org.billcarsonfr.jsonviewer.JSonViewerDialog import org.billcarsonfr.jsonviewer.JSonViewerDialog
import org.commonmark.parser.Parser 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.extensions.orFalse
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.content.ContentAttachmentData
@ -213,6 +217,7 @@ import java.net.URL
import java.util.UUID import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
import android.animation.ValueAnimator
@Parcelize @Parcelize
data class RoomDetailArgs( data class RoomDetailArgs(
@ -307,7 +312,7 @@ class RoomDetailFragment @Inject constructor(
private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
private var lockSendButton = false private var lockSendButton = false
private val knownCallsViewHolder = KnownCallsViewHolder() private val currentCallsViewPresenter = CurrentCallsViewPresenter()
private lateinit var emojiPopup: EmojiPopup private lateinit var emojiPopup: EmojiPopup
@ -321,6 +326,7 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
lifecycle.addObserver(JitsiBroadcastEventObserver(vectorBaseActivity, this::onBroadcastJitsiEvent))
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
knownCallsViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java) knownCallsViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java)
@ -344,9 +350,9 @@ class RoomDetailFragment @Inject constructor(
setupJumpToReadMarkerView() setupJumpToReadMarkerView()
setupActiveCallView() setupActiveCallView()
setupJumpToBottomView() setupJumpToBottomView()
setupConfBannerView()
setupEmojiPopup() setupEmojiPopup()
setupFailedMessagesWarningView() setupFailedMessagesWarningView()
setupRemoveJitsiWidgetView()
setupVoiceMessageView() setupVoiceMessageView()
views.roomToolbarContentView.debouncedClicks { views.roomToolbarContentView.debouncedClicks {
@ -363,7 +369,7 @@ class RoomDetailFragment @Inject constructor(
knownCallsViewModel knownCallsViewModel
.liveKnownCalls .liveKnownCalls
.observe(viewLifecycleOwner, { .observe(viewLifecycleOwner, {
knownCallsViewHolder.updateCall(callManager.getCurrentCall(), it) currentCallsViewPresenter.updateCall(callManager.getCurrentCall(), it)
invalidateOptionsMenu() invalidateOptionsMenu()
}) })
@ -412,6 +418,7 @@ class RoomDetailFragment @Inject constructor(
RoomDetailViewEvents.OpenActiveWidgetBottomSheet -> onViewWidgetsClicked() RoomDetailViewEvents.OpenActiveWidgetBottomSheet -> onViewWidgetsClicked()
is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message) is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message)
is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo) is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo)
RoomDetailViewEvents.LeaveJitsiConference -> leaveJitsiConference()
RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView() RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView()
RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView() RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView()
is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it) 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() { private fun onCannotRecord() {
// Update the UI, cancel the animation // Update the UI, cancel the animation
views.voiceMessageRecorderView.initVoiceRecordingViews() 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() { private fun setupEmojiPopup() {
emojiPopup = EmojiPopup emojiPopup = EmojiPopup
.Builder .Builder
@ -769,7 +771,7 @@ class RoomDetailFragment @Inject constructor(
override fun onDestroyView() { override fun onDestroyView() {
timelineEventController.callback = null timelineEventController.callback = null
timelineEventController.removeModelBuildListener(modelBuildListener) timelineEventController.removeModelBuildListener(modelBuildListener)
views.activeCallView.callback = null currentCallsViewPresenter.unBind()
modelBuildListener = null modelBuildListener = null
autoCompleter.clear() autoCompleter.clear()
debouncer.cancelAll() debouncer.cancelAll()
@ -780,7 +782,6 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onDestroy() { override fun onDestroy() {
knownCallsViewHolder.unBind()
roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
super.onDestroy() super.onDestroy()
} }
@ -816,12 +817,7 @@ class RoomDetailFragment @Inject constructor(
} }
private fun setupActiveCallView() { private fun setupActiveCallView() {
knownCallsViewHolder.bind( currentCallsViewPresenter.bind(views.currentCallsView, this)
views.activeCallPiP,
views.activeCallView,
views.activeCallPiPWrap,
this
)
} }
private fun navigateToEvent(action: RoomDetailViewEvents.NavigateToEvent) { private fun navigateToEvent(action: RoomDetailViewEvents.NavigateToEvent) {
@ -872,6 +868,22 @@ class RoomDetailFragment @Inject constructor(
onOptionsItemSelected(menuItem) 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) { override fun onPrepareOptionsMenu(menu: Menu) {
@ -880,7 +892,8 @@ class RoomDetailFragment @Inject constructor(
} }
withState(roomDetailViewModel) { state -> withState(roomDetailViewModel) { state ->
// Set the visual state of the call buttons (voice/video) to enabled/disabled according to user permissions // 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 1 -> false
2 -> state.isAllowedToStartWebRTCCall 2 -> state.isAllowedToStartWebRTCCall
else -> state.isAllowedToManageWidgets else -> state.isAllowedToManageWidgets
@ -891,14 +904,8 @@ class RoomDetailFragment @Inject constructor(
val matrixAppsMenuItem = menu.findItem(R.id.open_matrix_apps) val matrixAppsMenuItem = menu.findItem(R.id.open_matrix_apps)
val widgetsCount = state.activeRoomWidgets.invoke()?.size ?: 0 val widgetsCount = state.activeRoomWidgets.invoke()?.size ?: 0
if (widgetsCount > 0) { val hasOnlyJitsiWidget = widgetsCount == 1 && state.hasActiveJitsiWidget()
val actionView = matrixAppsMenuItem.actionView if (widgetsCount == 0 || hasOnlyJitsiWidget) {
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 {
// icon should be default color no badge // icon should be default color no badge
val actionView = matrixAppsMenuItem.actionView val actionView = matrixAppsMenuItem.actionView
actionView actionView
@ -906,6 +913,13 @@ class RoomDetailFragment @Inject constructor(
.setColorFilter(ThemeUtils.getColor(requireContext(), R.attr.vctr_content_secondary)) .setColorFilter(ThemeUtils.getColor(requireContext(), R.attr.vctr_content_secondary))
actionView.findViewById<TextView>(R.id.cart_badge).isVisible = false actionView.findViewById<TextView>(R.id.cart_badge).isVisible = false
matrixAppsMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER) 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() callActionsHandler.onVideoCallClicked()
true true
} }
R.id.hangup_call -> {
roomDetailViewModel.handle(RoomDetailAction.EndCall)
true
}
R.id.search -> { R.id.search -> {
handleSearchAction() handleSearchAction()
true true
@ -1362,7 +1372,7 @@ class RoomDetailFragment @Inject constructor(
invalidateOptionsMenu() invalidateOptionsMenu()
val summary = state.asyncRoomSummary() val summary = state.asyncRoomSummary()
renderToolbar(summary, state.typingMessage) renderToolbar(summary, state.typingMessage)
views.activeConferenceView.render(state) views.removeJitsiWidgetView.render(state)
views.failedMessagesWarningView.render(state.hasFailedSending) views.failedMessagesWarningView.render(state.hasFailedSending)
val inviter = state.asyncInviter() val inviter = state.asyncInviter()
if (summary?.membership == Membership.JOIN) { if (summary?.membership == Membership.JOIN) {

View File

@ -45,6 +45,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents() data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents()
data class JoinJitsiConference(val widget: Widget, val withVideo: Boolean) : RoomDetailViewEvents() data class JoinJitsiConference(val widget: Widget, val withVideo: Boolean) : RoomDetailViewEvents()
object LeaveJitsiConference : RoomDetailViewEvents()
object OpenInvitePeople : RoomDetailViewEvents() object OpenInvitePeople : RoomDetailViewEvents()
object OpenSetRoomAvatarDialog : 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.mvrx.runCatchingToAsync
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider 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.attachments.toContentAttachmentData
import im.vector.app.features.call.conference.JitsiService import im.vector.app.features.call.conference.JitsiService
import im.vector.app.features.call.lookup.CallProtocolsChecker 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.composer.rainbow.RainbowGenerator
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler 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.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.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
@ -66,6 +66,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer 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.MatrixCallback
import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
@ -115,12 +116,12 @@ class RoomDetailViewModel @AssistedInject constructor(
private val session: Session, private val session: Session,
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider, private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider,
private val stickerPickerActionHandler: StickerPickerActionHandler, private val stickerPickerActionHandler: StickerPickerActionHandler,
private val roomSummariesHolder: RoomSummariesHolder,
private val typingHelper: TypingHelper, private val typingHelper: TypingHelper,
private val callManager: WebRtcCallManager, private val callManager: WebRtcCallManager,
private val chatEffectManager: ChatEffectManager, private val chatEffectManager: ChatEffectManager,
private val directRoomHelper: DirectRoomHelper, private val directRoomHelper: DirectRoomHelper,
private val jitsiService: JitsiService, private val jitsiService: JitsiService,
private val activeConferenceHolder: JitsiActiveConferenceHolder,
private val voiceMessageHelper: VoiceMessageHelper, private val voiceMessageHelper: VoiceMessageHelper,
private val voicePlayerHelper: VoicePlayerHelper, private val voicePlayerHelper: VoicePlayerHelper,
timelineFactory: TimelineFactory timelineFactory: TimelineFactory
@ -241,8 +242,24 @@ class RoomDetailViewModel @AssistedInject constructor(
.map { widgets -> .map { widgets ->
widgets.filter { it.isActive } widgets.filter { it.isActive }
} }
.execute { .execute { widgets ->
copy(activeRoomWidgets = it) copy(activeRoomWidgets = widgets)
}
asyncSubscribe(RoomDetailViewState::activeRoomWidgets) { widgets ->
setState {
val jitsiWidget = widgets.firstOrNull { it.type == WidgetType.Jitsi }
val jitsiConfId = jitsiWidget?.let {
jitsiService.extractJitsiWidgetData(it)?.confId
}
copy(
jitsiState = jitsiState.copy(
confId = jitsiConfId,
widgetId = jitsiWidget?.widgetId,
hasJoined = activeConferenceHolder.isJoined(jitsiConfId)
)
)
}
} }
} }
@ -308,6 +325,9 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.EndCall -> handleEndCall() is RoomDetailAction.EndCall -> handleEndCall()
is RoomDetailAction.ManageIntegrations -> handleManageIntegrations() is RoomDetailAction.ManageIntegrations -> handleManageIntegrations()
is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action) 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.RemoveWidget -> handleDeleteWidget(action.widgetId)
is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action) is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action)
is RoomDetailAction.CancelSend -> handleCancel(action) is RoomDetailAction.CancelSend -> handleCancel(action)
@ -340,6 +360,33 @@ class RoomDetailViewModel @AssistedInject constructor(
}.exhaustive }.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) { private fun handleAcceptCall(action: RoomDetailAction.AcceptCall) {
callManager.getCallById(action.callId)?.also { callManager.getCallById(action.callId)?.also {
_viewEvents.post(RoomDetailViewEvents.DisplayAndAcceptCall(it)) _viewEvents.post(RoomDetailViewEvents.DisplayAndAcceptCall(it))
@ -448,10 +495,15 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
} }
private fun handleDeleteWidget(widgetId: String) { private fun handleDeleteWidget(widgetId: String) = withState { state ->
_viewEvents.post(RoomDetailViewEvents.ShowWaitingView) val isJitsiWidget = state.jitsiState.widgetId == widgetId
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
if (isJitsiWidget) {
setState { copy(jitsiState = jitsiState.copy(deleteWidgetInProgress = true)) }
} else {
_viewEvents.post(RoomDetailViewEvents.ShowWaitingView)
}
session.widgetService().destroyRoomWidget(room.roomId, widgetId) session.widgetService().destroyRoomWidget(room.roomId, widgetId)
// local echo // local echo
setState { setState {
@ -467,10 +519,14 @@ class RoomDetailViewModel @AssistedInject constructor(
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.failed_to_remove_widget))) _viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.failed_to_remove_widget)))
} finally { } finally {
if (isJitsiWidget) {
setState { copy(jitsiState = jitsiState.copy(deleteWidgetInProgress = false)) }
} else {
_viewEvents.post(RoomDetailViewEvents.HideWaitingView) _viewEvents.post(RoomDetailViewEvents.HideWaitingView)
} }
} }
} }
}
private fun handleCheckWidgetAllowed(action: RoomDetailAction.EnsureNativeWidgetAllowed) { private fun handleCheckWidgetAllowed(action: RoomDetailAction.EnsureNativeWidgetAllowed) {
val widget = action.widget val widget = action.widget
@ -682,9 +738,10 @@ class RoomDetailViewModel @AssistedInject constructor(
R.id.timeline_setting -> true R.id.timeline_setting -> true
R.id.invite -> state.canInvite R.id.invite -> state.canInvite
R.id.open_matrix_apps -> true R.id.open_matrix_apps -> true
R.id.voice_call, R.id.voice_call -> state.isWebRTCCallOptionAvailable()
R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty() R.id.video_call -> state.isWebRTCCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined
R.id.hangup_call -> callManager.getCallsByRoomId(state.roomId).isNotEmpty() // 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.search -> true
R.id.dev_tools -> vectorPreferences.developerMode() R.id.dev_tools -> vectorPreferences.developerMode()
else -> false else -> false
@ -1515,7 +1572,6 @@ class RoomDetailViewModel @AssistedInject constructor(
private fun observeSummaryState() { private fun observeSummaryState() {
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary -> asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
roomSummariesHolder.set(summary)
setState { setState {
val typingMessage = typingHelper.getTypingMessage(summary.typingUsers) val typingMessage = typingHelper.getTypingMessage(summary.typingUsers)
copy( copy(
@ -1563,7 +1619,6 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
override fun onCleared() { override fun onCleared() {
roomSummariesHolder.remove(room.roomId)
timeline.dispose() timeline.dispose()
timeline.removeAllListeners() timeline.removeAllListeners()
if (vectorPreferences.sendTypingNotifs()) { 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.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized 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.events.model.Event
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary 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.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.sync.SyncState 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.Widget
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
/** /**
* Describes the current send mode: * Describes the current send mode:
@ -55,6 +57,14 @@ sealed class UnreadState {
data class HasUnread(val firstUnreadEventId: String) : 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( data class RoomDetailViewState(
val roomId: String, val roomId: String,
val eventId: String?, val eventId: String?,
@ -75,7 +85,8 @@ data class RoomDetailViewState(
val canInvite: Boolean = true, val canInvite: Boolean = true,
val isAllowedToManageWidgets: Boolean = false, val isAllowedToManageWidgets: Boolean = false,
val isAllowedToStartWebRTCCall: Boolean = true, val isAllowedToStartWebRTCCall: Boolean = true,
val hasFailedSending: Boolean = false val hasFailedSending: Boolean = false,
val jitsiState: JitsiState = JitsiState()
) : MvRxState { ) : MvRxState {
constructor(args: RoomDetailArgs) : this( constructor(args: RoomDetailArgs) : this(
@ -85,5 +96,11 @@ data class RoomDetailViewState(
highlightedEventId = args.eventId 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 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.core.utils.checkPermissions
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
class StartCallActionsHandler( class StartCallActionsHandler(
private val roomId: String, private val roomId: String,
@ -61,16 +60,8 @@ class StartCallActionsHandler(
} }
2 -> { 2 -> {
val currentCall = callManager.getCurrentCall() val currentCall = callManager.getCurrentCall()
if (currentCall != null) { if (currentCall?.signalingRoomId == roomId) {
// resume existing if same room, if not prompt to kill and then restart new call?
if (currentCall.signalingRoomId == roomId) {
onTapToReturnToCall() onTapToReturnToCall()
}
// else {
// TODO might not work well, and should prompt
// webRtcPeerConnectionManager.endCall()
// safeStartCall(it, isVideoCall)
// }
} else if (!state.isAllowedToStartWebRTCCall) { } else if (!state.isAllowedToStartWebRTCCall) {
showDialogWithMessage(fragment.getString( showDialogWithMessage(fragment.getString(
if (state.isDm()) { if (state.isDm()) {
@ -96,9 +87,8 @@ class StartCallActionsHandler(
} }
)) ))
} else { } else {
if (state.activeRoomWidgets()?.filter { it.type == WidgetType.Jitsi }?.any() == true) { if (state.hasActiveJitsiWidget()) {
// A conference is already in progress! // A conference is already in progress, return
showDialogWithMessage(fragment.getString(R.string.conference_call_in_progress))
} else { } else {
MaterialAlertDialogBuilder(fragment.requireContext()) MaterialAlertDialogBuilder(fragment.requireContext())
.setTitle(if (isVideoCall) R.string.video_meeting else R.string.audio_meeting) .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.localDateTime
import im.vector.app.core.extensions.nextOrNull import im.vector.app.core.extensions.nextOrNull
import im.vector.app.core.extensions.prevOrNull import im.vector.app.core.extensions.prevOrNull
import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.features.home.room.detail.JitsiState
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.detail.RoomDetailAction 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.RoomDetailViewState
import im.vector.app.features.home.room.detail.UnreadState 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.ReadReceiptsItemFactory
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory 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.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.ContentDownloadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.TimelineControllerInterceptorHelper 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.TimelineEventVisibilityHelper
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener 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.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.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.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.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData 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.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.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer 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.Membership
import org.matrix.android.sdk.api.session.room.model.ReadReceipt 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.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.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent 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 timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val mergedHeaderItemFactory: MergedHeaderItemFactory, private val mergedHeaderItemFactory: MergedHeaderItemFactory,
private val session: Session, private val session: Session,
private val callManager: WebRtcCallManager,
@TimelineEventControllerHandler @TimelineEventControllerHandler
private val backgroundHandler: Handler, private val backgroundHandler: Handler,
private val userPreferencesProvider: UserPreferencesProvider,
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper, private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper,
private val readReceiptsItemFactory: ReadReceiptsItemFactory private val readReceiptsItemFactory: ReadReceiptsItemFactory
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { ) : 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 : interface Callback :
BaseCallback, BaseCallback,
ReactionPillCallback, ReactionPillCallback,
@ -149,14 +165,15 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
// Map eventId to adapter position // Map eventId to adapter position
private val adapterPositionMapping = HashMap<String, Int>() private val adapterPositionMapping = HashMap<String, Int>()
private val timelineEventsGroups = TimelineEventsGroups()
private val receiptsByEvent = HashMap<String, MutableList<ReadReceipt>>()
private val modelCache = arrayListOf<CacheItemData?>() private val modelCache = arrayListOf<CacheItemData?>()
private var currentSnapshot: List<TimelineEvent> = emptyList() private var currentSnapshot: List<TimelineEvent> = emptyList()
private var inSubmitList: Boolean = false private var inSubmitList: Boolean = false
private var hasReachedInvite: Boolean = false private var hasReachedInvite: Boolean = false
private var hasUTD: Boolean = false private var hasUTD: Boolean = false
private var unreadState: UnreadState = UnreadState.Unknown
private var positionOfReadMarker: Int? = null private var positionOfReadMarker: Int? = null
private var eventIdToHighlight: String? = null private var partialState: PartialState = PartialState()
var callback: Callback? = null var callback: Callback? = null
var timeline: Timeline? = 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. // 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 invalidatedSenderId: String? = currentSnapshot.getOrNull(position)?.senderInfo?.userId
val prevDisplayableEventIndex = currentSnapshot.subList(0, position).indexOfLast { val prevDisplayableEventIndex = currentSnapshot.subList(0, position).indexOfLast {
timelineEventVisibilityHelper.shouldShowEvent(it, eventIdToHighlight) timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId)
} }
if (prevDisplayableEventIndex != -1 && currentSnapshot[prevDisplayableEventIndex].senderInfo.userId == invalidatedSenderId) { if (prevDisplayableEventIndex != -1 && currentSnapshot[prevDisplayableEventIndex].senderInfo.userId == invalidatedSenderId) {
modelCache[prevDisplayableEventIndex] = null modelCache[prevDisplayableEventIndex] = null
@ -215,9 +232,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private val interceptorHelper = TimelineControllerInterceptorHelper( private val interceptorHelper = TimelineControllerInterceptorHelper(
::positionOfReadMarker, ::positionOfReadMarker,
adapterPositionMapping, adapterPositionMapping
userPreferencesProvider,
callManager
) )
init { init {
@ -226,29 +241,22 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
} }
override fun intercept(models: MutableList<EpoxyModel<*>>) = synchronized(modelCache) { 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) { fun update(viewState: RoomDetailViewState) = synchronized(modelCache) {
var requestModelBuild = false val newPartialState = PartialState(viewState)
if (eventIdToHighlight != viewState.highlightedEventId) { if (partialState.highlightedEventId != newPartialState.highlightedEventId) {
// Clear cache to force a refresh // Clear cache to force a refresh
synchronized(modelCache) {
for (i in 0 until modelCache.size) { for (i in 0 until modelCache.size) {
if (modelCache[i]?.eventId == viewState.highlightedEventId if (modelCache[i]?.eventId == viewState.highlightedEventId
|| modelCache[i]?.eventId == eventIdToHighlight) { || modelCache[i]?.eventId == partialState.highlightedEventId) {
modelCache[i] = null modelCache[i] = null
} }
} }
} }
eventIdToHighlight = viewState.highlightedEventId if (newPartialState != partialState) {
requestModelBuild = true partialState = newPartialState
}
if (this.unreadState != viewState.unreadState) {
this.unreadState = viewState.unreadState
requestModelBuild = true
}
if (requestModelBuild) {
requestModelBuild() requestModelBuild()
} }
} }
@ -346,31 +354,33 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
if (modelCache.isEmpty()) { if (modelCache.isEmpty()) {
return return
} }
val receiptsByEvents = getReadReceiptsByShownEvent() preprocessReverseEvents()
val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(receiptsByEvents) val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(receiptsByEvent)
(0 until modelCache.size).forEach { position -> (0 until modelCache.size).forEach { position ->
val event = currentSnapshot[position] val event = currentSnapshot[position]
val nextEvent = currentSnapshot.nextOrNull(position) val nextEvent = currentSnapshot.nextOrNull(position)
val prevEvent = currentSnapshot.prevOrNull(position) val prevEvent = currentSnapshot.prevOrNull(position)
val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull { val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull {
timelineEventVisibilityHelper.shouldShowEvent(it, eventIdToHighlight) timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId)
} }
// Should be build if not cached or if model should be refreshed
if (modelCache[position] == null || modelCache[position]?.isCacheable == false) {
val timelineEventsGroup = timelineEventsGroups.getOrNull(event)
val params = TimelineItemFactoryParams( val params = TimelineItemFactoryParams(
event = event, event = event,
prevEvent = prevEvent, prevEvent = prevEvent,
nextEvent = nextEvent, nextEvent = nextEvent,
nextDisplayableEvent = nextDisplayableEvent, nextDisplayableEvent = nextDisplayableEvent,
highlightedEventId = eventIdToHighlight, partialState = partialState,
lastSentEventIdWithoutReadReceipts = lastSentEventWithoutReadReceipts, lastSentEventIdWithoutReadReceipts = lastSentEventWithoutReadReceipts,
callback = callback callback = callback,
eventsGroup = timelineEventsGroup
) )
// Should be build if not cached or if model should be refreshed
if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild == true) {
modelCache[position] = buildCacheItem(params) modelCache[position] = buildCacheItem(params)
} }
val itemCachedData = modelCache[position] ?: return@forEach val itemCachedData = modelCache[position] ?: return@forEach
// Then update with additional models if needed // 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.id(event.localId)
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) 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( return CacheItemData(
localId = event.localId, localId = event.localId,
eventId = event.root.eventId, eventId = event.root.eventId,
eventModel = eventModel, eventModel = eventModel,
shouldTriggerBuild = shouldTriggerBuild) isCacheable = isCacheable
)
} }
private fun CacheItemData.enrichWithModels(event: TimelineEvent, private fun CacheItemData.enrichWithModels(event: TimelineEvent,
@ -399,10 +410,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val wantsDateSeparator = wantsDateSeparator(event, nextEvent) val wantsDateSeparator = wantsDateSeparator(event, nextEvent)
val mergedHeaderModel = mergedHeaderItemFactory.create(event, val mergedHeaderModel = mergedHeaderItemFactory.create(event,
nextEvent = nextEvent, nextEvent = nextEvent,
partialState = partialState,
items = this@TimelineEventController.currentSnapshot, items = this@TimelineEventController.currentSnapshot,
addDaySeparator = wantsDateSeparator, addDaySeparator = wantsDateSeparator,
currentPosition = position, currentPosition = position,
eventIdToHighlight = eventIdToHighlight, eventIdToHighlight = partialState.highlightedEventId,
callback = callback callback = callback
) { ) {
requestModelBuild() requestModelBuild()
@ -431,7 +443,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
return null return null
} }
// If the event is not shown, we go to the next one // If the event is not shown, we go to the next one
if (!timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) { if (!timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId)) {
continue continue
} }
// If the event is sent by us, we update the holder with the eventId and stop the search // 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 return null
} }
private fun getReadReceiptsByShownEvent(): Map<String, List<ReadReceipt>> { private fun preprocessReverseEvents() {
val receiptsByEvent = HashMap<String, MutableList<ReadReceipt>>() receiptsByEvent.clear()
if (!userPreferencesProvider.shouldShowReadReceipts()) { timelineEventsGroups.clear()
return receiptsByEvent
}
var lastShownEventId: String? = null
val itr = currentSnapshot.listIterator(currentSnapshot.size) val itr = currentSnapshot.listIterator(currentSnapshot.size)
var lastShownEventId: String? = null
while (itr.hasPrevious()) { while (itr.hasPrevious()) {
val event = itr.previous() val event = itr.previous()
timelineEventsGroups.addOrIgnore(event)
val currentReadReceipts = ArrayList(event.readReceipts).filter { val currentReadReceipts = ArrayList(event.readReceipts).filter {
it.user.userId != session.myUserId it.user.userId != session.myUserId
} }
if (timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) { if (timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId)) {
lastShownEventId = event.eventId lastShownEventId = event.eventId
} }
if (lastShownEventId == null) { if (lastShownEventId == null) {
@ -463,7 +474,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val existingReceipts = receiptsByEvent.getOrPut(lastShownEventId) { ArrayList() } val existingReceipts = receiptsByEvent.getOrPut(lastShownEventId) { ArrayList() }
existingReceipts.addAll(currentReadReceipts) existingReceipts.addAll(currentReadReceipts)
} }
return receiptsByEvent
} }
private fun buildDaySeparatorItem(originServerTs: Long?): DaySeparatorItem { private fun buildDaySeparatorItem(originServerTs: Long?): DaySeparatorItem {
@ -536,6 +546,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val eventModel: EpoxyModel<*>? = null, val eventModel: EpoxyModel<*>? = null,
val mergedHeaderModel: BasedMergedItem<*>? = null, val mergedHeaderModel: BasedMergedItem<*>? = null,
val formattedDayModel: DaySeparatorItem? = 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.powerlevel.PowerLevelsObservableFactory
import im.vector.app.features.reactions.data.EmojiDataSource import im.vector.app.features.reactions.data.EmojiDataSource
import im.vector.app.features.settings.VectorPreferences 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.Session
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
@ -207,7 +208,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
EventType.CALL_CANDIDATES, EventType.CALL_CANDIDATES,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> { EventType.CALL_ANSWER -> {
noticeEventFormatter.format(timelineEvent) noticeEventFormatter.format(timelineEvent, room?.roomSummary()?.isDirect.orFalse())
} }
else -> null else -> null
} }

View File

@ -16,127 +16,122 @@
package im.vector.app.features.home.room.detail.timeline.factory package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.features.call.vectorCallService import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider 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.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider 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.MessageInformationDataFactory
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory 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.CallTileTimelineItem_ import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData 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.Session
import org.matrix.android.sdk.api.session.events.model.EventType 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.RoomSummary
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.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject import javax.inject.Inject
class CallItemFactory @Inject constructor( class CallItemFactory @Inject constructor(
private val session: Session, private val session: Session,
private val userPreferencesProvider: UserPreferencesProvider,
private val messageColorProvider: MessageColorProvider, private val messageColorProvider: MessageColorProvider,
private val messageInformationDataFactory: MessageInformationDataFactory, private val messageInformationDataFactory: MessageInformationDataFactory,
private val messageItemAttributesFactory: MessageItemAttributesFactory, private val messageItemAttributesFactory: MessageItemAttributesFactory,
private val avatarSizeProvider: AvatarSizeProvider, private val avatarSizeProvider: AvatarSizeProvider,
private val roomSummariesHolder: RoomSummariesHolder, private val noticeItemFactory: NoticeItemFactory) {
private val callManager: WebRtcCallManager
) {
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event val event = params.event
if (event.root.eventId == null) return null 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 informationData = messageInformationDataFactory.create(params)
val callSignalingContent = event.getCallSignalingContent() ?: return null val callKind = if (callEventGrouper.isVideo()) CallTileTimelineItem.CallKind.VIDEO else CallTileTimelineItem.CallKind.AUDIO
val callId = callSignalingContent.callId ?: return null val callItem = when (event.root.getClearType()) {
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()) {
EventType.CALL_ANSWER -> { EventType.CALL_ANSWER -> {
if (callEventGrouper.isInCall()) {
createCallTileTimelineItem( createCallTileTimelineItem(
roomId = roomId, roomSummary = roomSummary,
callId = callId, callId = callEventGrouper.callId,
callStatus = CallTileTimelineItem.CallStatus.IN_CALL, callStatus = CallTileTimelineItem.CallStatus.IN_CALL,
callKind = callKind, callKind = callKind,
callback = params.callback, callback = params.callback,
highlight = params.isHighlighted, highlight = params.isHighlighted,
informationData = informationData, informationData = informationData,
isStillActive = call != null isStillActive = callEventGrouper.isInCall(),
formattedDuration = callEventGrouper.formattedDuration()
) )
} else {
null
}
} }
EventType.CALL_INVITE -> { EventType.CALL_INVITE -> {
if (callEventGrouper.isRinging()) {
createCallTileTimelineItem( createCallTileTimelineItem(
roomId = roomId, roomSummary = roomSummary,
callId = callId, callId = callEventGrouper.callId,
callStatus = CallTileTimelineItem.CallStatus.INVITED, callStatus = CallTileTimelineItem.CallStatus.INVITED,
callKind = callKind, callKind = callKind,
callback = params.callback, callback = params.callback,
highlight = params.isHighlighted, highlight = params.isHighlighted,
informationData = informationData, informationData = informationData,
isStillActive = call != null isStillActive = callEventGrouper.isRinging(),
formattedDuration = callEventGrouper.formattedDuration()
) )
} else {
null
}
} }
EventType.CALL_REJECT -> { EventType.CALL_REJECT -> {
createCallTileTimelineItem( createCallTileTimelineItem(
roomId = roomId, roomSummary = roomSummary,
callId = callId, callId = callEventGrouper.callId,
callStatus = CallTileTimelineItem.CallStatus.REJECTED, callStatus = CallTileTimelineItem.CallStatus.REJECTED,
callKind = callKind, callKind = callKind,
callback = params.callback, callback = params.callback,
highlight = params.isHighlighted, highlight = params.isHighlighted,
informationData = informationData, informationData = informationData,
isStillActive = false isStillActive = false,
formattedDuration = callEventGrouper.formattedDuration()
) )
} }
EventType.CALL_HANGUP -> { EventType.CALL_HANGUP -> {
createCallTileTimelineItem( createCallTileTimelineItem(
roomId = roomId, roomSummary = roomSummary,
callId = callId, callId = callEventGrouper.callId,
callStatus = CallTileTimelineItem.CallStatus.ENDED, callStatus = if (callEventGrouper.callWasMissed()) CallTileTimelineItem.CallStatus.MISSED else CallTileTimelineItem.CallStatus.ENDED,
callKind = callKind, callKind = callKind,
callback = params.callback, callback = params.callback,
highlight = params.isHighlighted, highlight = params.isHighlighted,
informationData = informationData, informationData = informationData,
isStillActive = false isStillActive = false,
formattedDuration = callEventGrouper.formattedDuration()
) )
} }
else -> null else -> null
} }
} return if (callItem == null && showHiddenEvents) {
// Fallback to notice item for showing hidden events
private fun TimelineEvent.getCallSignalingContent(): CallSignalingContent? { noticeItemFactory.create(params)
return when (root.getClearType()) { } else {
EventType.CALL_INVITE -> root.getClearContent().toModel<CallInviteContent>() callItem
EventType.CALL_HANGUP -> root.getClearContent().toModel<CallHangupContent>()
EventType.CALL_REJECT -> root.getClearContent().toModel<CallRejectContent>()
EventType.CALL_ANSWER -> root.getClearContent().toModel<CallAnswerContent>()
else -> null
} }
} }
private fun createCallTileTimelineItem( private fun createCallTileTimelineItem(
roomId: String, roomSummary: RoomSummary,
callId: String, callId: String,
callKind: CallTileTimelineItem.CallKind, callKind: CallTileTimelineItem.CallKind,
callStatus: CallTileTimelineItem.CallStatus, callStatus: CallTileTimelineItem.CallStatus,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
isStillActive: Boolean, isStillActive: Boolean,
formattedDuration: String,
callback: TimelineEventController.Callback? callback: TimelineEventController.Callback?
): CallTileTimelineItem? { ): CallTileTimelineItem? {
val correctedRoomId = session.vectorCallService.userMapper.nativeRoomForVirtualRoom(roomId) ?: roomId val userOfInterest = roomSummary.toMatrixItem()
val userOfInterest = roomSummariesHolder.get(correctedRoomId)?.toMatrixItem() ?: return null
val attributes = messageItemAttributesFactory.create(null, informationData, callback).let { val attributes = messageItemAttributesFactory.create(null, informationData, callback).let {
CallTileTimelineItem.Attributes( CallTileTimelineItem.Attributes(
callId = callId, callId = callId,
@ -144,6 +139,7 @@ class CallItemFactory @Inject constructor(
callStatus = callStatus, callStatus = callStatus,
informationData = informationData, informationData = informationData,
avatarRenderer = it.avatarRenderer, avatarRenderer = it.avatarRenderer,
formattedDuration = formattedDuration,
messageColorProvider = messageColorProvider, messageColorProvider = messageColorProvider,
itemClickListener = it.itemClickListener, itemClickListener = it.itemClickListener,
itemLongClickListener = it.itemLongClickListener, 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.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider 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.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.TimelineEventVisibilityHelper
import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged
import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration 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, class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val avatarSizeProvider: AvatarSizeProvider, private val avatarSizeProvider: AvatarSizeProvider,
private val roomSummariesHolder: RoomSummariesHolder, private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
private val collapsedEventIds = linkedSetOf<Long>() private val collapsedEventIds = linkedSetOf<Long>()
private val mergeItemCollapseStates = HashMap<Long, Boolean>() private val mergeItemCollapseStates = HashMap<Long, Boolean>()
@ -60,6 +58,7 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
items: List<TimelineEvent>, items: List<TimelineEvent>,
partialState: TimelineEventController.PartialState,
addDaySeparator: Boolean, addDaySeparator: Boolean,
currentPosition: Int, currentPosition: Int,
eventIdToHighlight: String?, eventIdToHighlight: String?,
@ -70,18 +69,17 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
&& event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel<RoomCreateContent>()?.creator)) { && event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel<RoomCreateContent>()?.creator)) {
// It's the first item before room.create // It's the first item before room.create
// Collapse all room configuration events // 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)) { } else if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) {
null null
} else { } 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, private fun buildMembershipEventsMergedSummary(currentPosition: Int,
items: List<TimelineEvent>, items: List<TimelineEvent>,
partialState: TimelineEventController.PartialState,
event: TimelineEvent, event: TimelineEvent,
eventIdToHighlight: String?, eventIdToHighlight: String?,
requestModelBuild: () -> Unit, requestModelBuild: () -> Unit,
@ -102,7 +100,7 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
memberName = mergedEvent.senderInfo.disambiguatedDisplayName, memberName = mergedEvent.senderInfo.disambiguatedDisplayName,
localId = mergedEvent.localId, localId = mergedEvent.localId,
eventId = mergedEvent.root.eventId ?: "", eventId = mergedEvent.root.eventId ?: "",
isDirectRoom = isDirectRoom(event.roomId) isDirectRoom = partialState.isDirectRoom()
) )
mergedData.add(data) mergedData.add(data)
} }
@ -141,6 +139,7 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
private fun buildRoomCreationMergedSummary(currentPosition: Int, private fun buildRoomCreationMergedSummary(currentPosition: Int,
items: List<TimelineEvent>, items: List<TimelineEvent>,
partialState: TimelineEventController.PartialState,
event: TimelineEvent, event: TimelineEvent,
eventIdToHighlight: String?, eventIdToHighlight: String?,
requestModelBuild: () -> Unit, requestModelBuild: () -> Unit,
@ -173,7 +172,7 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
memberName = mergedEvent.senderInfo.disambiguatedDisplayName, memberName = mergedEvent.senderInfo.disambiguatedDisplayName,
localId = mergedEvent.localId, localId = mergedEvent.localId,
eventId = mergedEvent.root.eventId ?: "", eventId = mergedEvent.root.eventId ?: "",
isDirectRoom = isDirectRoom(event.roomId) isDirectRoom = partialState.isDirectRoom()
) )
mergedData.add(data) mergedData.add(data)
} }
@ -206,7 +205,7 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM, isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM,
callback = callback, callback = callback,
currentUserId = currentUserId, currentUserId = currentUserId,
roomSummary = roomSummariesHolder.get(event.roomId), roomSummary = partialState.roomSummary,
canChangeAvatar = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_AVATAR) ?: false, canChangeAvatar = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_AVATAR) ?: false,
canChangeTopic = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_TOPIC) ?: false, canChangeTopic = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_TOPIC) ?: false,
canChangeName = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_NAME) ?: false canChangeName = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_NAME) ?: false
@ -223,6 +222,10 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
} else null } else null
} }
private fun TimelineEventController.PartialState.isDirectRoom(): Boolean {
return roomSummary?.isDirect.orFalse()
}
fun isCollapsed(localId: Long): Boolean { fun isCollapsed(localId: Long): Boolean {
return collapsedEventIds.contains(localId) 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.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 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 import javax.inject.Inject
class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEventFormatter, 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? { fun create(params: TimelineItemFactoryParams): NoticeItem? {
val event = params.event 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 informationData = informationDataFactory.create(params)
val attributes = NoticeItem.Attributes( val attributes = NoticeItem.Attributes(
avatarRenderer = avatarRenderer, avatarRenderer = avatarRenderer,

View File

@ -17,6 +17,7 @@
package im.vector.app.features.home.room.detail.timeline.factory 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.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroup
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
data class TimelineItemFactoryParams( data class TimelineItemFactoryParams(
@ -24,9 +25,14 @@ data class TimelineItemFactoryParams(
val prevEvent: TimelineEvent? = null, val prevEvent: TimelineEvent? = null,
val nextEvent: TimelineEvent? = null, val nextEvent: TimelineEvent? = null,
val nextDisplayableEvent: TimelineEvent? = null, val nextDisplayableEvent: TimelineEvent? = null,
val highlightedEventId: String? = null, val partialState: TimelineEventController.PartialState = TimelineEventController.PartialState(),
val lastSentEventIdWithoutReadReceipts: String? = null, 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 val isHighlighted = highlightedEventId == event.eventId
} }

View File

@ -16,34 +16,28 @@
package im.vector.app.features.home.room.detail.timeline.factory 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.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.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.MessageInformationDataFactory
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
import im.vector.app.features.home.room.detail.timeline.item.WidgetTileTimelineItem import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem_
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 org.matrix.android.sdk.api.session.events.model.toModel 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.WidgetContent
import org.matrix.android.sdk.api.session.widgets.model.WidgetType import org.matrix.android.sdk.api.session.widgets.model.WidgetType
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject import javax.inject.Inject
class WidgetItemFactory @Inject constructor( class WidgetItemFactory @Inject constructor(
private val sp: StringProvider,
private val messageItemAttributesFactory: MessageItemAttributesFactory,
private val informationDataFactory: MessageInformationDataFactory, private val informationDataFactory: MessageInformationDataFactory,
private val noticeItemFactory: NoticeItemFactory, private val noticeItemFactory: NoticeItemFactory,
private val avatarSizeProvider: AvatarSizeProvider, private val avatarSizeProvider: AvatarSizeProvider,
private val activeSessionDataSource: ActiveSessionDataSource private val messageColorProvider: MessageColorProvider,
) { private val avatarRenderer: AvatarRenderer,
private val currentUserId: String? private val userPreferencesProvider: UserPreferencesProvider) {
get() = activeSessionDataSource.currentValue?.orNull()?.myUserId
private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event val event = params.event
@ -51,62 +45,54 @@ class WidgetItemFactory @Inject constructor(
val previousWidgetContent: WidgetContent? = event.root.resolvedPrevContent().toModel() val previousWidgetContent: WidgetContent? = event.root.resolvedPrevContent().toModel()
return when (WidgetType.fromString(widgetContent.type ?: previousWidgetContent?.type ?: "")) { 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 // There is lot of other widget types we could improve here
else -> noticeItemFactory.create(params) else -> noticeItemFactory.create(params)
} }
} }
private fun createJitsiItem(params: TimelineItemFactoryParams, private fun createJitsiItem(params: TimelineItemFactoryParams, widgetContent: WidgetContent): VectorEpoxyModel<*>? {
widgetContent: WidgetContent,
previousWidgetContent: WidgetContent?): VectorEpoxyModel<*> {
val timelineEvent = params.event
val informationData = informationDataFactory.create(params) val informationData = informationDataFactory.create(params)
val attributes = messageItemAttributesFactory.create(null, informationData, params.callback) val userOfInterest = params.partialState.roomSummary?.toMatrixItem() ?: return null
val isActiveTile = widgetContent.isActive()
val disambiguatedDisplayName = timelineEvent.senderInfo.disambiguatedDisplayName val jitsiWidgetEventsGroup = params.eventsGroup?.let { JitsiWidgetEventsGroup(it) } ?: return null
val message = if (widgetContent.isActive()) { val isCallStillActive = jitsiWidgetEventsGroup.isStillActive()
val widgetName = widgetContent.getHumanName() val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents()
if (previousWidgetContent?.isActive().orFalse()) { if (isActiveTile && !isCallStillActive) {
// Widget has been modified return if (showHiddenEvents) {
if (timelineEvent.root.isSentByCurrentUser()) { noticeItemFactory.create(params)
sp.getString(R.string.notice_widget_jitsi_modified_by_you, widgetName)
} else { } else {
sp.getString(R.string.notice_widget_jitsi_modified, disambiguatedDisplayName, widgetName) null
}
} 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)
} }
} }
val callStatus = if (isActiveTile && params.event.root.stateKey == params.partialState.jitsiState.widgetId) {
if (params.partialState.jitsiState.hasJoined) {
CallTileTimelineItem.CallStatus.IN_CALL
} else { } else {
// Widget has been removed CallTileTimelineItem.CallStatus.INVITED
val widgetName = previousWidgetContent?.getHumanName() }
if (timelineEvent.root.isSentByCurrentUser()) {
sp.getString(R.string.notice_widget_jitsi_removed_by_you, widgetName)
} else { } else {
sp.getString(R.string.notice_widget_jitsi_removed, disambiguatedDisplayName, widgetName) CallTileTimelineItem.CallStatus.ENDED
} }
} val attributes = CallTileTimelineItem.Attributes(
callId = jitsiWidgetEventsGroup.callId,
return WidgetTileTimelineItem_() callKind = CallTileTimelineItem.CallKind.CONFERENCE,
.attributes( callStatus = callStatus,
WidgetTileTimelineItem.Attributes(
title = message,
drawableStart = R.drawable.ic_video,
informationData = informationData, informationData = informationData,
avatarRenderer = attributes.avatarRenderer, avatarRenderer = avatarRenderer,
messageColorProvider = attributes.messageColorProvider, messageColorProvider = messageColorProvider,
itemLongClickListener = attributes.itemLongClickListener, itemClickListener = null,
itemClickListener = attributes.itemClickListener, itemLongClickListener = null,
reactionPillCallback = attributes.reactionPillCallback, reactionPillCallback = params.callback,
readReceiptsCallback = attributes.readReceiptsCallback, readReceiptsCallback = params.callback,
emojiTypeFace = attributes.emojiTypeFace userOfInterest = userOfInterest,
) callback = params.callback,
isStillActive = isCallStillActive,
formattedDuration = ""
) )
return CallTileTimelineItem_()
.attributes(attributes)
.highlighted(params.isHighlighted)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
} }
} }

View File

@ -41,7 +41,7 @@ class DisplayableEventFormatter @Inject constructor(
private val noticeEventFormatter: NoticeEventFormatter private val noticeEventFormatter: NoticeEventFormatter
) { ) {
fun format(timelineEvent: TimelineEvent, appendAuthor: Boolean): CharSequence { fun format(timelineEvent: TimelineEvent, isDm: Boolean, appendAuthor: Boolean): CharSequence {
if (timelineEvent.root.isRedacted()) { if (timelineEvent.root.isRedacted()) {
return noticeEventFormatter.formatRedactedEvent(timelineEvent.root) return noticeEventFormatter.formatRedactedEvent(timelineEvent.root)
} }
@ -135,7 +135,7 @@ class DisplayableEventFormatter @Inject constructor(
} }
else -> { else -> {
return span { return span {
text = noticeEventFormatter.format(timelineEvent) ?: "" text = noticeEventFormatter.format(timelineEvent, isDm) ?: ""
textStyle = "italic" 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.ActiveSessionDataSource
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.resources.StringProvider 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.roomprofile.permissions.RoleFormatter
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.extensions.appendNl 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.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.RoomNameContent 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.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.RoomThirdPartyInviteContent
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent 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 roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
private val roleFormatter: RoleFormatter, private val roleFormatter: RoleFormatter,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val roomSummariesHolder: RoomSummariesHolder,
private val sp: StringProvider private val sp: StringProvider
) { ) {
@ -67,28 +64,25 @@ class NoticeEventFormatter @Inject constructor(
private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId
private fun RoomSummary?.isDm() = this?.isDirect.orFalse() fun format(timelineEvent: TimelineEvent, isDm: Boolean): CharSequence? {
fun format(timelineEvent: TimelineEvent): CharSequence? {
val rs = roomSummariesHolder.get(timelineEvent.roomId)
return when (val type = timelineEvent.root.getClearType()) { return when (val type = timelineEvent.root.getClearType()) {
EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs) EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm)
EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root, rs) EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root, isDm)
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(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_AVATAR -> formatRoomAvatarEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(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, rs) 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_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_HISTORY_VISIBILITY -> 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_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_ENCRYPTION -> formatRoomEncryptionEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_WIDGET, EventType.STATE_ROOM_WIDGET,
EventType.STATE_ROOM_WIDGET_LEGACY -> formatWidgetEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) 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.STATE_ROOM_POWER_LEVELS -> formatRoomPowerLevels(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_CANDIDATES, 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()) { 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_NAME -> formatRoomNameEvent(event, senderName)
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName) EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName)
EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(event, senderName) EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(event, senderName)
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName, rs) EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName, isDm)
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(event, senderName, rs) EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(event, senderName, isDm)
EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName, rs) EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName, isDm)
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_REJECT, EventType.CALL_REJECT,
EventType.CALL_ANSWER -> formatCallEvent(type, event, senderName) EventType.CALL_ANSWER -> formatCallEvent(type, event, senderName)
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName, rs) EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName, isDm)
else -> { else -> {
Timber.v("Type $type not handled by this formatter") Timber.v("Type $type not handled by this formatter")
null null
@ -201,14 +195,14 @@ class NoticeEventFormatter @Inject constructor(
return "Debug: event type \"${event.getClearType()}\"" 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>() return event.getClearContent().toModel<RoomCreateContent>()
?.takeIf { it.creator.isNullOrBlank().not() } ?.takeIf { it.creator.isNullOrBlank().not() }
?.let { ?.let {
if (event.isSentByCurrentUser()) { 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 { } 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()) { 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 { } 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 historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility ?: return null
val historyVisibilitySuffix = roomHistoryVisibilityFormatter.getNoticeSuffix(historyVisibility) val historyVisibilitySuffix = roomHistoryVisibilityFormatter.getNoticeSuffix(historyVisibility)
return if (event.isSentByCurrentUser()) { 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) historyVisibilitySuffix)
} else { } 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) 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 content = event.getClearContent().toModel<RoomThirdPartyInviteContent>()
val prevContent = event.resolvedPrevContent()?.toModel<RoomThirdPartyInviteContent>() val prevContent = event.resolvedPrevContent()?.toModel<RoomThirdPartyInviteContent>()
@ -294,24 +288,24 @@ class NoticeEventFormatter @Inject constructor(
// Revoke case // Revoke case
if (event.isSentByCurrentUser()) { if (event.isSentByCurrentUser()) {
sp.getString( sp.getString(
if (rs.isDm()) { if (isDm) {
R.string.notice_direct_room_third_party_revoked_invite_by_you R.string.notice_direct_room_third_party_revoked_invite_by_you
} else { } else {
R.string.notice_room_third_party_revoked_invite_by_you R.string.notice_room_third_party_revoked_invite_by_you
}, },
prevContent.displayName) prevContent.displayName)
} else { } 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) senderName, prevContent.displayName)
} }
} }
content != null -> { content != null -> {
// Invitation case // Invitation case
if (event.isSentByCurrentUser()) { 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) content.displayName)
} else { } 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) senderName, content.displayName)
} }
} }
@ -358,7 +352,7 @@ class NoticeEventFormatter @Inject constructor(
} }
EventType.CALL_REJECT -> EventType.CALL_REJECT ->
if (event.isSentByCurrentUser()) { if (event.isSentByCurrentUser()) {
sp.getString(R.string.call_tile_you_declined, "") sp.getString(R.string.call_tile_you_declined_this_call)
} else { } else {
sp.getString(R.string.call_tile_other_declined, senderName) 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 eventContent: RoomMemberContent? = event.getClearContent().toModel()
val prevEventContent: RoomMemberContent? = event.resolvedPrevContent().toModel() val prevEventContent: RoomMemberContent? = event.resolvedPrevContent().toModel()
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
|| eventContent?.membership == Membership.LEAVE || eventContent?.membership == Membership.LEAVE
return if (isMembershipEvent) { return if (isMembershipEvent) {
buildMembershipNotice(event, senderName, eventContent, prevEventContent, rs) buildMembershipNotice(event, senderName, eventContent, prevEventContent, isDm)
} else { } else {
buildProfileNotice(event, senderName, eventContent, prevEventContent) 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() val eventContent: RoomGuestAccessContent? = event.getClearContent().toModel()
return when (eventContent?.guestAccess) { return when (eventContent?.guestAccess) {
GuestAccess.CanJoin -> GuestAccess.CanJoin ->
if (event.isSentByCurrentUser()) { if (event.isSentByCurrentUser()) {
sp.getString( 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 { } 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) senderName)
} }
GuestAccess.Forbidden -> GuestAccess.Forbidden ->
if (event.isSentByCurrentUser()) { if (event.isSentByCurrentUser()) {
sp.getString( 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 { } 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) senderName)
} }
else -> null else -> null
@ -656,7 +650,7 @@ class NoticeEventFormatter @Inject constructor(
senderName: String?, senderName: String?,
eventContent: RoomMemberContent?, eventContent: RoomMemberContent?,
prevEventContent: RoomMemberContent?, prevEventContent: RoomMemberContent?,
rs: RoomSummary?): String? { isDm: Boolean): String? {
val senderDisplayName = senderName ?: event.senderId ?: "" val senderDisplayName = senderName ?: event.senderId ?: ""
val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: event.stateKey ?: "" val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: event.stateKey ?: ""
return when (eventContent?.membership) { return when (eventContent?.membership) {
@ -706,17 +700,17 @@ class NoticeEventFormatter @Inject constructor(
Membership.JOIN -> Membership.JOIN ->
eventContent.safeReason?.let { reason -> eventContent.safeReason?.let { reason ->
if (event.isSentByCurrentUser()) { 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) reason)
} else { } 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) senderDisplayName, reason)
} }
} ?: run { } ?: run {
if (event.isSentByCurrentUser()) { 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 { } 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) senderDisplayName)
} }
} }
@ -738,7 +732,7 @@ class NoticeEventFormatter @Inject constructor(
eventContent.safeReason?.let { reason -> eventContent.safeReason?.let { reason ->
if (event.isSentByCurrentUser()) { if (event.isSentByCurrentUser()) {
sp.getString( sp.getString(
if (rs.isDm()) { if (isDm) {
R.string.notice_direct_room_leave_with_reason_by_you R.string.notice_direct_room_leave_with_reason_by_you
} else { } else {
R.string.notice_room_leave_with_reason_by_you R.string.notice_room_leave_with_reason_by_you
@ -746,14 +740,14 @@ class NoticeEventFormatter @Inject constructor(
reason reason
) )
} else { } 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) senderDisplayName, reason)
} }
} ?: run { } ?: run {
if (event.isSentByCurrentUser()) { 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 { } 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) 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 val content = event.getClearContent().toModel<RoomJoinRulesContent>() ?: return null
return when (content.joinRules) { return when (content.joinRules) {
RoomJoinRules.INVITE -> RoomJoinRules.INVITE ->
if (event.isSentByCurrentUser()) { 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 { } 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) senderName)
} }
RoomJoinRules.PUBLIC -> 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.isAttachmentMessage
import org.matrix.android.sdk.api.session.events.model.toModel 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.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.model.message.MessageVerificationRequestContent
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent 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 * 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, class MessageInformationDataFactory @Inject constructor(private val session: Session,
private val roomSummariesHolder: RoomSummariesHolder,
private val dateFormatter: VectorDateFormatter, private val dateFormatter: VectorDateFormatter,
private val visibilityHelper: TimelineEventVisibilityHelper, private val visibilityHelper: TimelineEventVisibilityHelper,
private val vectorPreferences: VectorPreferences) { private val vectorPreferences: VectorPreferences) {
@ -74,7 +74,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|| nextDisplayableEvent.isEdition() || nextDisplayableEvent.isEdition()
val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE) 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 // SendState Decoration
val isSentByMe = event.root.senderId == session.myUserId 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 { private fun getE2EDecoration(roomSummary: RoomSummary?, event: TimelineEvent): E2EDecoration {
val roomSummary = roomSummariesHolder.get(event.roomId)
return if ( return if (
event.root.sendState == SendState.SYNCED event.root.sendState == SendState.SYNCED
&& roomSummary?.isEncrypted.orFalse() && 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.EpoxyModel
import com.airbnb.epoxy.VisibilityState import com.airbnb.epoxy.VisibilityState
import im.vector.app.core.epoxy.LoadingItem_ 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.UnreadState
import im.vector.app.features.home.room.detail.timeline.TimelineEventController 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.DaySeparatorItem
import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_ 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 private const val DEFAULT_PREFETCH_THRESHOLD = 30
class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMutableProperty0<Int?>, class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMutableProperty0<Int?>,
private val adapterPositionMapping: MutableMap<String, Int>, private val adapterPositionMapping: MutableMap<String, Int>
private val userPreferencesProvider: UserPreferencesProvider,
private val callManager: WebRtcCallManager
) { ) {
private var previousModelsSize = 0 private var previousModelsSize = 0
@ -50,14 +44,12 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
) { ) {
positionOfReadMarker.set(null) positionOfReadMarker.set(null)
adapterPositionMapping.clear() adapterPositionMapping.clear()
val callIds = mutableSetOf<String>()
// Add some prefetch loader if needed // Add some prefetch loader if needed
models.addBackwardPrefetchIfNeeded(timeline, callback) models.addBackwardPrefetchIfNeeded(timeline, callback)
models.addForwardPrefetchIfNeeded(timeline, callback) models.addForwardPrefetchIfNeeded(timeline, callback)
val modelsIterator = models.listIterator() val modelsIterator = models.listIterator()
val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents()
var index = 0 var index = 0
val firstUnreadEventId = (unreadState as? UnreadState.HasUnread)?.firstUnreadEventId val firstUnreadEventId = (unreadState as? UnreadState.HasUnread)?.firstUnreadEventId
var atLeastOneVisibleItemSinceLastDaySeparator = false var atLeastOneVisibleItemSinceLastDaySeparator = false
@ -83,11 +75,6 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
return@forEach return@forEach
} }
atLeastOneVisibleItemSinceLastDaySeparator = false atLeastOneVisibleItemSinceLastDaySeparator = false
} else if (epoxyModel is CallTileTimelineItem) {
val hasBeenRemoved = modelsIterator.removeCallItemIfNeeded(epoxyModel, callIds, showHiddenEvents)
if (!hasBeenRemoved) {
atLeastOneVisibleItemSinceLastDaySeparator = true
}
} }
if (appendReadMarker) { if (appendReadMarker) {
modelsIterator.addReadMarkerItem(callback) modelsIterator.addReadMarkerItem(callback)
@ -109,29 +96,6 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
add(readMarker) 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?) { private fun MutableList<EpoxyModel<*>>.addBackwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) {
val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false
if (shouldAddBackwardPrefetch) { 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 override val baseAttributes: AbsBaseMessageItem.Attributes
get() = attributes get() = attributes
override fun isCacheable(): Boolean {
return attributes.informationData.sendStateDecoration != SendStateDecoration.SENT
}
@EpoxyAttribute @EpoxyAttribute
lateinit var attributes: Attributes lateinit var attributes: Attributes

View File

@ -15,6 +15,7 @@
*/ */
package im.vector.app.features.home.room.detail.timeline.item package im.vector.app.features.home.room.detail.timeline.item
import android.content.res.Resources
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button 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.ClickListener
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setLeftDrawable 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.AvatarRenderer
import im.vector.app.features.home.room.detail.RoomDetailAction 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.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import timber.log.Timber
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state) @EpoxyModelClass(layout = R.layout.item_timeline_event_base_state)
abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Holder>() { abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Holder>() {
@ -45,6 +44,8 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
override val baseAttributes: AbsBaseMessageItem.Attributes override val baseAttributes: AbsBaseMessageItem.Attributes
get() = attributes get() = attributes
override fun isCacheable() = false
@EpoxyAttribute @EpoxyAttribute
lateinit var attributes: Attributes lateinit var attributes: Attributes
@ -57,15 +58,130 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
} }
holder.creatorNameView.text = attributes.userOfInterest.getBestName() holder.creatorNameView.text = attributes.userOfInterest.getBestName()
attributes.avatarRenderer.render(attributes.userOfInterest, holder.creatorAvatarView) attributes.avatarRenderer.render(attributes.userOfInterest, holder.creatorAvatarView)
if (attributes.callKind != CallKind.UNKNOWN) { when (attributes.callStatus) {
holder.callKindView.isVisible = true CallStatus.INVITED -> renderInvitedStatus(holder)
holder.callKindView.setText(attributes.callKind.title) CallStatus.IN_CALL -> renderInCallStatus(holder)
holder.callKindView.setLeftDrawable(attributes.callKind.icon) CallStatus.REJECTED -> renderRejectedStatus(holder)
} else { CallStatus.ENDED -> renderEndedStatus(holder)
holder.callKindView.isVisible = false CallStatus.MISSED -> renderMissedStatus(holder)
}
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)
}
} 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)
}
} }
if (attributes.callStatus == CallStatus.INVITED && !attributes.informationData.sentByMe && attributes.isStillActive) {
holder.acceptRejectViewGroup.isVisible = true 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)
}
}
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 { holder.acceptView.onClick {
attributes.callback?.onTimelineItemAction(RoomDetailAction.AcceptCall(callId = attributes.callId)) attributes.callback?.onTimelineItemAction(RoomDetailAction.AcceptCall(callId = attributes.callId))
} }
@ -73,65 +189,59 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
holder.rejectView.onClick { holder.rejectView.onClick {
attributes.callback?.onTimelineItemAction(RoomDetailAction.EndCall) attributes.callback?.onTimelineItemAction(RoomDetailAction.EndCall)
} }
holder.statusView.isVisible = false if (attributes.callKind == CallKind.AUDIO) {
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)
}
CallKind.AUDIO -> {
holder.rejectView.setText(R.string.call_notification_reject) holder.rejectView.setText(R.string.call_notification_reject)
holder.acceptView.setText(R.string.call_notification_answer) holder.acceptView.setText(R.string.call_notification_answer)
holder.acceptView.setLeftDrawable(R.drawable.ic_call_audio_small, R.attr.colorOnPrimary) holder.acceptView.setLeftDrawable(R.drawable.ic_call_audio_small, R.attr.colorOnPrimary)
} } else if (attributes.callKind == CallKind.VIDEO) {
CallKind.VIDEO -> {
holder.rejectView.setText(R.string.call_notification_reject) holder.rejectView.setText(R.string.call_notification_reject)
holder.acceptView.setText(R.string.call_notification_answer) holder.acceptView.setText(R.string.call_notification_answer)
holder.acceptView.setLeftDrawable(R.drawable.ic_call_video_small, R.attr.colorOnPrimary) holder.acceptView.setLeftDrawable(R.drawable.ic_call_video_small, R.attr.colorOnPrimary)
} }
}
else -> { else -> {
Timber.w("Shouldn't be in that state")
}
}
} else {
holder.acceptRejectViewGroup.isVisible = false holder.acceptRejectViewGroup.isVisible = false
holder.statusView.isVisible = true
} }
holder.statusView.setCallStatus(attributes) }
renderSendState(holder.view, null, holder.failedToSendIndicator) 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)
}
}
} }
private fun TextView.setCallStatus(attributes: Attributes) { private fun TextView.setStatus(@StringRes statusRes: Int, @DrawableRes drawableRes: Int? = null) {
when (attributes.callStatus) { val status = resources.getString(statusRes)
CallStatus.INVITED -> if (attributes.informationData.sentByMe) { setStatus(status, drawableRes)
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(status: String, @DrawableRes drawableRes: Int? = null) {
setLeftDrawable(drawableRes ?: attributes.callKind.icon)
text = status
} }
class Holder : AbsBaseMessageItem.Holder(STUB_ID) { class Holder : AbsBaseMessageItem.Holder(STUB_ID) {
val acceptView by bind<Button>(R.id.itemCallAcceptView) val acceptView by bind<Button>(R.id.itemCallAcceptView)
val rejectView by bind<Button>(R.id.itemCallRejectView) val rejectView by bind<Button>(R.id.itemCallRejectView)
val acceptRejectViewGroup by bind<ViewGroup>(R.id.itemCallAcceptRejectViewGroup) 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 creatorAvatarView by bind<ImageView>(R.id.itemCallCreatorAvatar)
val creatorNameView by bind<TextView>(R.id.itemCallCreatorNameTextView) val creatorNameView by bind<TextView>(R.id.itemCallCreatorNameTextView)
val statusView by bind<TextView>(R.id.itemCallStatusTextView) val statusView by bind<TextView>(R.id.itemCallStatusTextView)
val endGuideline by bind<View>(R.id.messageEndGuideline) val endGuideline by bind<View>(R.id.messageEndGuideline)
val failedToSendIndicator by bind<ImageView>(R.id.messageFailToSendIndicator) val failedToSendIndicator by bind<ImageView>(R.id.messageFailToSendIndicator)
val resources: Resources
get() = view.context.resources
} }
companion object { companion object {
@ -144,6 +254,7 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
val callStatus: CallStatus, val callStatus: CallStatus,
val userOfInterest: MatrixItem, val userOfInterest: MatrixItem,
val isStillActive: Boolean, val isStillActive: Boolean,
val formattedDuration: String,
val callback: TimelineEventController.Callback? = null, val callback: TimelineEventController.Callback? = null,
override val informationData: MessageInformationData, override val informationData: MessageInformationData,
override val avatarRenderer: AvatarRenderer, 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) { enum class CallKind(@DrawableRes val icon: Int, @StringRes val title: Int) {
VIDEO(R.drawable.ic_call_video_small, R.string.action_video_call), VIDEO(R.drawable.ic_call_video_small, R.string.action_video_call),
AUDIO(R.drawable.ic_call_audio_small, R.string.action_voice_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), CONFERENCE(R.drawable.ic_call_video_small, R.string.action_video_call);
UNKNOWN(0, 0)
val isVoiceCall
get() = this == AUDIO
} }
enum class CallStatus { enum class CallStatus {
INVITED, INVITED,
IN_CALL, IN_CALL,
REJECTED, REJECTED,
MISSED,
ENDED; ENDED;
fun isActive() = this == INVITED || this == IN_CALL fun isActive() = this == INVITED || this == IN_CALL

View File

@ -26,4 +26,9 @@ interface ItemWithEvents {
fun canAppendReadMarker(): Boolean = true fun canAppendReadMarker(): Boolean = true
fun isVisible(): 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

@ -131,7 +131,7 @@ class RoomListViewModel @Inject constructor(
updatableQuery = it updatableQuery = it
}, },
suggestedRoomJoiningState, suggestedRoomJoiningState,
vectorPreferences.labsSpacesOnlyOrphansInHome() !vectorPreferences.prefSpacesShowAllRoomInHome()
) )
} else { } else {
RoomListSectionBuilderGroup( RoomListSectionBuilderGroup(

View File

@ -114,7 +114,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
var latestEventTime: CharSequence = "" var latestEventTime: CharSequence = ""
val latestEvent = roomSummary.latestPreviewableEvent val latestEvent = roomSummary.latestPreviewableEvent
if (latestEvent != null) { 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) latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
} }
val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers) 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.core.resources.StringProvider
import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
import im.vector.app.features.home.room.detail.timeline.format.NoticeEventFormatter 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.Session
import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
@ -135,7 +136,7 @@ class NotifiableEventResolver @Inject constructor(
if (room == null) { if (room == null) {
Timber.e("## Unable to resolve room for eventId [$event]") Timber.e("## Unable to resolve room for eventId [$event]")
// Ok room is not known in store, but we can still display something // 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 roomName = stringProvider.getString(R.string.notification_unknown_room_name)
val senderDisplayName = event.senderInfo.disambiguatedDisplayName 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 roomName = room.roomSummary()?.displayName ?: ""
val senderDisplayName = event.senderInfo.disambiguatedDisplayName val senderDisplayName = event.senderInfo.disambiguatedDisplayName
@ -209,7 +210,7 @@ class NotifiableEventResolver @Inject constructor(
val roomId = event.roomId ?: return null val roomId = event.roomId ?: return null
val dName = event.senderId?.let { session.getRoomMember(it, roomId)?.displayName } val dName = event.senderId?.let { session.getRoomMember(it, roomId)?.displayName }
if (Membership.INVITE == content.membership) { 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) ?: stringProvider.getString(R.string.notification_new_invitation)
return InviteNotifiableEvent( return InviteNotifiableEvent(
session.myUserId, 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) val builder = NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
.setContentTitle(ensureTitleNotEmpty(title)) .setContentTitle(ensureTitleNotEmpty(title))
.apply { .apply {
setContentText(stringProvider.getString(R.string.call_ring)) setContentText(stringProvider.getString(R.string.call_ringing))
if (call.mxCall.isVideoCall) { if (call.mxCall.isVideoCall) {
setSmallIcon(R.drawable.ic_call_answer_video) setSmallIcon(R.drawable.ic_call_answer_video)
} else { } 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 { companion object {
const val INCOMING_CALL_PRIORITY = Int.MAX_VALUE const val INCOMING_CALL_PRIORITY = Int.MAX_VALUE
const val JITSI_CALL_PRIORITY = INCOMING_CALL_PRIORITY - 1
} }
private var weakCurrentActivity: WeakReference<Activity>? = null private var weakCurrentActivity: WeakReference<Activity>? = null

View File

@ -153,6 +153,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
const val SETTINGS_LABS_ALLOW_EXTENDED_LOGS = "SETTINGS_LABS_ALLOW_EXTENDED_LOGS" const val SETTINGS_LABS_ALLOW_EXTENDED_LOGS = "SETTINGS_LABS_ALLOW_EXTENDED_LOGS"
const val SETTINGS_LABS_USE_RESTRICTED_JOIN_RULE = "SETTINGS_LABS_USE_RESTRICTED_JOIN_RULE" const val SETTINGS_LABS_USE_RESTRICTED_JOIN_RULE = "SETTINGS_LABS_USE_RESTRICTED_JOIN_RULE"
const val SETTINGS_LABS_SPACES_HOME_AS_ORPHAN = "SETTINGS_LABS_SPACES_HOME_AS_ORPHAN" const val SETTINGS_LABS_SPACES_HOME_AS_ORPHAN = "SETTINGS_LABS_SPACES_HOME_AS_ORPHAN"
const val SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME = "SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME"
private const val SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY" private const val SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY"
private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY" private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
@ -971,10 +972,16 @@ class VectorPreferences @Inject constructor(private val context: Context) {
return defaultPrefs.getBoolean(SETTINGS_LABS_USE_RESTRICTED_JOIN_RULE, false) return defaultPrefs.getBoolean(SETTINGS_LABS_USE_RESTRICTED_JOIN_RULE, false)
} }
fun labsSpacesOnlyOrphansInHome(): Boolean { private fun labsSpacesOnlyOrphansInHome(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_LABS_SPACES_HOME_AS_ORPHAN, false) return defaultPrefs.getBoolean(SETTINGS_LABS_SPACES_HOME_AS_ORPHAN, false)
} }
fun prefSpacesShowAllRoomInHome(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME,
// migration of old property
!labsSpacesOnlyOrphansInHome())
}
/* /*
* Photo / video picker * Photo / video picker
*/ */

View File

@ -17,24 +17,12 @@
package im.vector.app.features.settings package im.vector.app.features.settings
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.preference.VectorSwitchPreference
import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs
import javax.inject.Inject import javax.inject.Inject
class VectorSettingsLabsFragment @Inject constructor( class VectorSettingsLabsFragment @Inject constructor() : VectorSettingsBaseFragment() {
private val vectorPreferences: VectorPreferences
) : VectorSettingsBaseFragment() {
override var titleRes = R.string.room_settings_labs_pref_title override var titleRes = R.string.room_settings_labs_pref_title
override val preferenceXmlRes = R.xml.vector_settings_labs override val preferenceXmlRes = R.xml.vector_settings_labs
override fun bindPref() { override fun bindPref() {}
findPreference<VectorSwitchPreference>(VectorPreferences.SETTINGS_LABS_SPACES_HOME_AS_ORPHAN)!!.let { pref ->
pref.setOnPreferenceChangeListener { _, _ ->
MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCache = false))
true
}
}
}
} }

View File

@ -27,7 +27,10 @@ import im.vector.app.core.dialogs.PhotoOrVideoDialog
import im.vector.app.core.extensions.restart import im.vector.app.core.extensions.restart
import im.vector.app.core.preference.VectorListPreference import im.vector.app.core.preference.VectorListPreference
import im.vector.app.core.preference.VectorPreference import im.vector.app.core.preference.VectorPreference
import im.vector.app.core.preference.VectorSwitchPreference
import im.vector.app.databinding.DialogSelectTextSizeBinding import im.vector.app.databinding.DialogSelectTextSizeBinding
import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs
import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.configuration.VectorConfiguration
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
import javax.inject.Inject import javax.inject.Inject
@ -67,6 +70,14 @@ class VectorSettingsPreferencesFragment @Inject constructor(
} }
} }
findPreference<VectorSwitchPreference>(VectorPreferences.SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME)!!.let { pref ->
pref.isChecked = vectorPreferences.prefSpacesShowAllRoomInHome()
pref.setOnPreferenceChangeListener { _, _ ->
MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCache = false))
true
}
}
// Url preview // Url preview
/* /*
TODO Note: we keep the setting client side for now TODO Note: we keep the setting client side for now

View File

@ -121,7 +121,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
roomSummaryQueryParams { roomSummaryQueryParams {
this.memberships = listOf(Membership.JOIN) this.memberships = listOf(Membership.JOIN)
this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null).takeIf { this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null).takeIf {
vectorPreferences.labsSpacesOnlyOrphansInHome() !vectorPreferences.prefSpacesShowAllRoomInHome()
} ?: ActiveSpaceFilter.None } ?: ActiveSpaceFilter.None
}, sortOrder = RoomSortOrder.NONE }, sortOrder = RoomSortOrder.NONE
).asObservable() ).asObservable()
@ -139,7 +139,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
roomSummaryQueryParams { roomSummaryQueryParams {
this.memberships = listOf(Membership.JOIN) this.memberships = listOf(Membership.JOIN)
this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null).takeIf { this.activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null).takeIf {
vectorPreferences.labsSpacesOnlyOrphansInHome() !vectorPreferences.prefSpacesShowAllRoomInHome()
} ?: ActiveSpaceFilter.None } ?: ActiveSpaceFilter.None
} }
) )

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 <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: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"/> android:fillColor="#737D8C"/>
</vector> </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" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="25dp" android:width="24dp"
android:height="25dp" android:height="24dp"
android:viewportWidth="25" android:viewportWidth="25"
android:viewportHeight="25"> android:viewportHeight="25">
<path <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: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"/> android:fillColor="#000000"/>
<path <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" 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" <vector android:height="24dp" android:viewportHeight="32"
android:width="24dp" android:viewportWidth="32" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
android:height="24dp"
android:viewportWidth="24" <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"/>
android:viewportHeight="24"> <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 <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"/>
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> </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,35 +22,55 @@
<org.webrtc.SurfaceViewRenderer <org.webrtc.SurfaceViewRenderer
android:id="@+id/fullscreenRenderer" android:id="@+id/fullscreenRenderer"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="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 <org.webrtc.SurfaceViewRenderer
android:id="@+id/pipRenderer" android:id="@+id/pipRenderer"
android:layout_width="wrap_content" android:layout_width="@dimen/call_pip_width"
android:layout_height="144dp" android:layout_height="@dimen/call_pip_height"
android:layout_gravity="bottom|end" android:visibility="gone"
android:layout_marginTop="16dp" tools:visibility="visible" />
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent" </com.google.android.material.card.MaterialCardView>
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/otherKnownCallLayout" android:id="@+id/otherKnownCallLayout"
android:layout_width="80dp" android:layout_width="@dimen/call_pip_width"
android:layout_height="144dp" android:layout_height="@dimen/call_pip_height"
android:layout_marginTop="32dp" android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:background="@color/element_background_light" android:background="@color/element_background_light"
android:foreground="?attr/selectableItemBackground" android:foreground="?attr/selectableItemBackground"
android:visibility="gone" android:visibility="gone"
app:cardBackgroundColor="@color/bg_call_screen" app:cardBackgroundColor="@color/bg_call_screen"
app:cardCornerRadius="4dp" app:cardCornerRadius="@dimen/call_pip_radius"
app:cardElevation="4dp" app:cardElevation="4dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toStartOf="@id/pipRendererWrapper"
tools:visibility="visible"> tools:visibility="visible">
<ImageView <ImageView
@ -72,10 +92,33 @@
</com.google.android.material.card.MaterialCardView> </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"
android:background="@android:color/transparent"
android:fitsSystemWindows="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
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 <ImageView
android:id="@+id/otherMemberAvatar" android:id="@+id/otherMemberAvatar"
android:layout_width="80dp" android:layout_width="120dp"
android:layout_height="80dp" android:layout_height="120dp"
android:contentDescription="@string/avatar"
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
@ -111,22 +154,6 @@
app:layout_constraintTop_toBottomOf="@id/otherMemberAvatar" app:layout_constraintTop_toBottomOf="@id/otherMemberAvatar"
tools:text="@sample/users.json/data/displayName" /> 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 <Button
android:id="@+id/callActionText" android:id="@+id/callActionText"
style="@style/Widget.Vector.Button.Text" style="@style/Widget.Vector.Button.Text"
@ -137,35 +164,15 @@
android:textColor="?colorSecondary" android:textColor="?colorSecondary"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/callStatusText" app:layout_constraintTop_toBottomOf="@id/participantNameText"
tools:text="@string/call_resume_action" /> 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 <androidx.constraintlayout.widget.Group
android:id="@+id/callInfoGroup" android:id="@+id/callInfoGroup"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="visible" android:visibility="visible"
app:constraint_referenced_ids="participantNameText, otherMemberAvatar,callStatusText" /> app:constraint_referenced_ids="participantNameText, otherMemberAvatar" />
<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" />
<im.vector.app.features.call.CallControlsView <im.vector.app.features.call.CallControlsView
android:id="@+id/callControlsView" android:id="@+id/callControlsView"
@ -173,16 +180,4 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" /> 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> </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:background="?colorSurface"
android:orientation="vertical"> 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 <im.vector.app.core.ui.views.BottomSheetActionButton
android:id="@+id/callControlsSwitchCamera" android:id="@+id/callControlsSwitchCamera"
android:layout_width="match_parent" 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" android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"> 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 <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/groupToolbar" android:id="@+id/groupToolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -128,41 +137,12 @@
app:layout_constraintTop_toBottomOf="@id/syncStateView" app:layout_constraintTop_toBottomOf="@id/syncStateView"
tools:visibility="visible" /> 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 <androidx.fragment.app.FragmentContainerView
android:id="@+id/roomListContainer" android:id="@+id/roomListContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView" app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"
app:layout_constraintTop_toBottomOf="@+id/activeCallView" /> app:layout_constraintTop_toBottomOf="@+id/homeKeysBackupBanner" />
<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>
<com.google.android.material.bottomnavigation.BottomNavigationView <com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigationView" android:id="@+id/bottomNavigationView"

View File

@ -12,6 +12,14 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"> 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 <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/roomToolbar" android:id="@+id/roomToolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -100,21 +108,15 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appBarLayout" /> app:layout_constraintTop_toBottomOf="@id/appBarLayout" />
<im.vector.app.core.ui.views.CurrentCallsView <im.vector.app.features.call.conference.RemoveJitsiWidgetView
android:id="@+id/activeCallView" android:id="@+id/removeJitsiWidgetView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone" android:background="?android:colorBackground"
app:layout_constraintTop_toBottomOf="@id/syncStateView" android:minHeight="54dp"
tools:visibility="visible" /> 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 <androidx.recyclerview.widget.RecyclerView
android:id="@+id/timelineRecyclerView" android:id="@+id/timelineRecyclerView"
@ -124,7 +126,7 @@
app:layout_constraintBottom_toTopOf="@+id/timelineRecyclerViewBarrier" app:layout_constraintBottom_toTopOf="@+id/timelineRecyclerViewBarrier"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="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" /> tools:listitem="@layout/item_timeline_event_base" />
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
@ -140,7 +142,7 @@
app:closeIcon="@drawable/ic_close_24dp" app:closeIcon="@drawable/ic_close_24dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/activeConferenceView" app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView"
tools:visibility="visible" /> tools:visibility="visible" />
@ -205,27 +207,6 @@
app:barrierDirection="top" app:barrierDirection="top"
app:constraint_referenced_ids="composerLayout,notificationAreaView, failedMessagesWarningView" /> 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 <im.vector.app.core.platform.BadgeFloatingActionButton
android:id="@+id/jumpToBottomView" android:id="@+id/jumpToBottomView"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -20,41 +20,29 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:layout_marginTop="4dp" android:layout_marginTop="8dp"
android:drawablePadding="6dp" android:drawablePadding="6dp"
android:gravity="center" android:gravity="center"
android:textColor="?vctr_content_primary" android:textColor="?vctr_content_primary"
android:textStyle="bold" android:textStyle="bold"
tools:text="@sample/users.json/data/displayName" /> 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 <TextView
android:id="@+id/itemCallStatusTextView" android:id="@+id/itemCallStatusTextView"
style="@style/Widget.Vector.TextView.Body" style="@style/Widget.Vector.TextView.Body"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginTop="8dp"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginBottom="12dp" android:layout_marginBottom="16dp"
android:gravity="center"
android:textColor="?vctr_content_secondary" 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 <androidx.constraintlayout.widget.ConstraintLayout
@ -80,7 +68,6 @@
style="@style/Widget.Vector.Button.Destructive" style="@style/Widget.Vector.Button.Destructive"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:minWidth="120dp" android:minWidth="120dp"
app:layout_constraintEnd_toStartOf="@+id/itemCallAcceptView" 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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="16dp" android:padding="16dp"
android:paddingStart="32dp" android:paddingStart="0dp"
android:paddingEnd="32dp" android:paddingEnd="0dp"
android:visibility="gone" android:visibility="gone"
tools:background="@color/password_strength_bar_low" tools:background="@color/password_strength_bar_low"
tools:layout_marginTop="120dp" tools:layout_marginTop="120dp"
tools:visibility="visible"> tools:visibility="visible">
<ImageView <ImageView
android:id="@+id/openChatIcon" android:id="@+id/audioSettingsIcon"
android:layout_width="@dimen/layout_touch_size" android:layout_width="56dp"
android:layout_height="@dimen/layout_touch_size" android:layout_height="56dp"
android:background="@drawable/bg_rounded_button" android:background="@drawable/bg_rounded_button"
android:clickable="true" android:clickable="true"
android:contentDescription="@string/a11y_open_chat" android:contentDescription="@string/call_select_sound_device"
android:focusable="true" android:focusable="true"
android:scaleType="center" 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" /> tools:ignore="MissingConstraints,MissingPrefix" />
<ImageView <ImageView
@ -85,8 +87,23 @@
android:clickable="true" android:clickable="true"
android:contentDescription="@string/a11y_mute_microphone" android:contentDescription="@string/a11y_mute_microphone"
android:focusable="true" android:focusable="true"
android:padding="16dp" android:padding="12dp"
android:src="@drawable/ic_microphone_off" 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:backgroundTint="?android:colorBackground"
app:tint="?vctr_content_primary" app:tint="?vctr_content_primary"
tools:ignore="MissingConstraints,MissingPrefix" /> tools:ignore="MissingConstraints,MissingPrefix" />
@ -105,24 +122,11 @@
app:tint="?colorOnPrimary" app:tint="?colorOnPrimary"
tools:ignore="MissingConstraints,MissingPrefix" /> 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 <ImageView
android:id="@+id/moreIcon" android:id="@+id/moreIcon"
android:layout_width="@dimen/layout_touch_size" android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size" android:layout_height="@dimen/layout_touch_size"
android:layout_marginTop="8dp"
android:background="@drawable/bg_rounded_button" android:background="@drawable/bg_rounded_button"
android:clickable="true" android:clickable="true"
android:contentDescription="@string/settings" android:contentDescription="@string/settings"
@ -135,9 +139,10 @@
<androidx.constraintlayout.helper.widget.Flow <androidx.constraintlayout.helper.widget.Flow
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" 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_horizontalGap="16dp"
app:flow_horizontalStyle="packed" app:flow_horizontalStyle="packed"
app:flow_wrapMode="chain"
tools:ignore="MissingConstraints" /> tools:ignore="MissingConstraints" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -6,39 +6,20 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?colorPrimary" android:background="?colorPrimary"
android:foreground="?attr/selectableItemBackground" android:foreground="?attr/selectableItemBackground"
tools:parentTag="android.widget.RelativeLayout"> tools:parentTag="android.widget.FrameLayout">
<TextView <TextView
android:id="@+id/currentCallsInfo" android:id="@+id/currentCallsInfo"
style="@style/Widget.Vector.TextView.Body" style="@style/Widget.Vector.TextView.Body"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_toStartOf="@id/returnToCallButton"
android:drawablePadding="10dp" android:drawablePadding="10dp"
android:gravity="center_vertical" android:gravity="center"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingTop="12dp" android:paddingTop="12dp"
android:paddingEnd="16dp" android:paddingEnd="16dp"
android:paddingBottom="12dp" android:paddingBottom="12dp"
android:text="@string/call_only_active" android:text="@string/call_only_active"
android:textColor="?colorOnPrimary" 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" />
</merge> </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>

Some files were not shown because too many files have changed in this diff Show More