Merge pull request #2830 from vector-im/feature/fga/fix_some_voip_issues
Feature/fga/fix some voip issues
This commit is contained in:
commit
fd94536118
|
@ -26,6 +26,7 @@
|
|||
<w>pkcs</w>
|
||||
<w>previewable</w>
|
||||
<w>previewables</w>
|
||||
<w>pstn</w>
|
||||
<w>riotx</w>
|
||||
<w>signin</w>
|
||||
<w>signout</w>
|
||||
|
|
|
@ -16,12 +16,11 @@
|
|||
|
||||
package org.matrix.android.sdk.api.session.call
|
||||
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
|
||||
interface CallSignalingService {
|
||||
|
||||
fun getTurnServer(callback: MatrixCallback<TurnServerResponse>): Cancelable
|
||||
suspend fun getTurnServer(): TurnServerResponse
|
||||
|
||||
fun getPSTNProtocolChecker(): PSTNProtocolChecker
|
||||
|
||||
/**
|
||||
* Create an outgoing call
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.api.session.call
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import org.matrix.android.sdk.internal.session.thirdparty.GetThirdPartyProtocolsTask
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val PSTN_VECTOR_KEY = "im.vector.protocol.pstn"
|
||||
private const val PSTN_MATRIX_KEY = "m.protocol.pstn"
|
||||
|
||||
/**
|
||||
* This class is responsible for checking if the HS support the PSTN protocol.
|
||||
* As long as the request succeed, it'll check only once by session.
|
||||
*/
|
||||
@SessionScope
|
||||
class PSTNProtocolChecker @Inject internal constructor(private val taskExecutor: TaskExecutor,
|
||||
private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask) {
|
||||
|
||||
interface Listener {
|
||||
fun onPSTNSupportUpdated()
|
||||
}
|
||||
|
||||
private var alreadyChecked = AtomicBoolean(false)
|
||||
|
||||
private val pstnSupportListeners = mutableListOf<Listener>()
|
||||
|
||||
fun addListener(listener: Listener) {
|
||||
pstnSupportListeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeListener(listener: Listener) {
|
||||
pstnSupportListeners.remove(listener)
|
||||
}
|
||||
|
||||
var supportedPSTNProtocol: String? = null
|
||||
private set
|
||||
|
||||
fun checkForPSTNSupportIfNeeded() {
|
||||
if (alreadyChecked.get()) return
|
||||
taskExecutor.executorScope.checkForPSTNSupport()
|
||||
}
|
||||
|
||||
private fun CoroutineScope.checkForPSTNSupport() = launch {
|
||||
try {
|
||||
supportedPSTNProtocol = getSupportedPSTN(3)
|
||||
alreadyChecked.set(true)
|
||||
if (supportedPSTNProtocol != null) {
|
||||
pstnSupportListeners.forEach {
|
||||
tryOrNull { it.onPSTNSupportUpdated() }
|
||||
}
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.v("Fail to get supported PSTN, will check again next time.")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getSupportedPSTN(maxTries: Int): String? {
|
||||
val thirdPartyProtocols: Map<String, ThirdPartyProtocol> = try {
|
||||
getThirdPartyProtocolsTask.execute(Unit)
|
||||
} catch (failure: Throwable) {
|
||||
if (maxTries == 1) {
|
||||
throw failure
|
||||
} else {
|
||||
// Wait for 10s before trying again
|
||||
delay(10_000L)
|
||||
return getSupportedPSTN(maxTries - 1)
|
||||
}
|
||||
}
|
||||
return when {
|
||||
thirdPartyProtocols.containsKey(PSTN_VECTOR_KEY) -> PSTN_VECTOR_KEY
|
||||
thirdPartyProtocols.containsKey(PSTN_MATRIX_KEY) -> PSTN_MATRIX_KEY
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,16 +16,12 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.call
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.call.CallListener
|
||||
import org.matrix.android.sdk.api.session.call.CallSignalingService
|
||||
import org.matrix.android.sdk.api.session.call.MxCall
|
||||
import org.matrix.android.sdk.api.session.call.PSTNProtocolChecker
|
||||
import org.matrix.android.sdk.api.session.call.TurnServerResponse
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.task.launchToCallback
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -34,14 +30,16 @@ internal class DefaultCallSignalingService @Inject constructor(
|
|||
private val callSignalingHandler: CallSignalingHandler,
|
||||
private val mxCallFactory: MxCallFactory,
|
||||
private val activeCallHandler: ActiveCallHandler,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val turnServerDataSource: TurnServerDataSource
|
||||
private val turnServerDataSource: TurnServerDataSource,
|
||||
private val pstnProtocolChecker: PSTNProtocolChecker
|
||||
) : CallSignalingService {
|
||||
|
||||
override fun getTurnServer(callback: MatrixCallback<TurnServerResponse>): Cancelable {
|
||||
return taskExecutor.executorScope.launchToCallback(Dispatchers.Default, callback) {
|
||||
turnServerDataSource.getTurnServer()
|
||||
}
|
||||
override suspend fun getTurnServer(): TurnServerResponse {
|
||||
return turnServerDataSource.getTurnServer()
|
||||
}
|
||||
|
||||
override fun getPSTNProtocolChecker(): PSTNProtocolChecker {
|
||||
return pstnProtocolChecker
|
||||
}
|
||||
|
||||
override fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall {
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2019 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.epoxy
|
||||
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_empty)
|
||||
abstract class TimelineEmptyItem : VectorEpoxyModel<TimelineEmptyItem.Holder>(), ItemWithEvents {
|
||||
|
||||
@EpoxyAttribute lateinit var eventId: String
|
||||
|
||||
override fun getEventIds(): List<String> {
|
||||
return listOf(eventId)
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder()
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.app.features.call
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
|
@ -29,17 +30,16 @@ import im.vector.app.core.platform.VectorViewModel
|
|||
import im.vector.app.features.call.audio.CallAudioManager
|
||||
import im.vector.app.features.call.webrtc.WebRtcCall
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.call.CallState
|
||||
import org.matrix.android.sdk.api.session.call.MxCall
|
||||
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
|
||||
import org.matrix.android.sdk.api.session.call.TurnServerResponse
|
||||
import org.matrix.android.sdk.api.session.room.model.call.supportCallTransfer
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
|
||||
class VectorCallViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: VectorCallViewState,
|
||||
|
@ -50,7 +50,7 @@ class VectorCallViewModel @AssistedInject constructor(
|
|||
|
||||
private var call: WebRtcCall? = null
|
||||
|
||||
private var connectionTimeoutTimer: Timer? = null
|
||||
private var connectionTimeoutJob: Job? = null
|
||||
private var hasBeenConnectedOnce = false
|
||||
|
||||
private val callListener = object : WebRtcCall.Listener {
|
||||
|
@ -92,26 +92,20 @@ class VectorCallViewModel @AssistedInject constructor(
|
|||
val callState = call.state
|
||||
if (callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
|
||||
hasBeenConnectedOnce = true
|
||||
connectionTimeoutTimer?.cancel()
|
||||
connectionTimeoutTimer = null
|
||||
connectionTimeoutJob?.cancel()
|
||||
connectionTimeoutJob = null
|
||||
} else {
|
||||
// do we reset as long as it's moving?
|
||||
connectionTimeoutTimer?.cancel()
|
||||
connectionTimeoutJob?.cancel()
|
||||
if (hasBeenConnectedOnce) {
|
||||
connectionTimeoutTimer = Timer().apply {
|
||||
schedule(object : TimerTask() {
|
||||
override fun run() {
|
||||
session.callSignalingService().getTurnServer(object : MatrixCallback<TurnServerResponse> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
_viewEvents.post(VectorCallViewEvents.ConnectionTimeout(null))
|
||||
}
|
||||
|
||||
override fun onSuccess(data: TurnServerResponse) {
|
||||
_viewEvents.post(VectorCallViewEvents.ConnectionTimeout(data))
|
||||
}
|
||||
})
|
||||
}
|
||||
}, 30_000)
|
||||
connectionTimeoutJob = viewModelScope.launch {
|
||||
delay(30_000)
|
||||
try {
|
||||
val turn = session.callSignalingService().getTurnServer()
|
||||
_viewEvents.post(VectorCallViewEvents.ConnectionTimeout(turn))
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(VectorCallViewEvents.ConnectionTimeout(null))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,20 +22,22 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
|
|||
import org.matrix.android.sdk.api.session.Session
|
||||
import javax.inject.Inject
|
||||
|
||||
class DialPadLookup @Inject constructor(val session: Session,
|
||||
val directRoomHelper: DirectRoomHelper,
|
||||
val callManager: WebRtcCallManager
|
||||
class DialPadLookup @Inject constructor(
|
||||
private val session: Session,
|
||||
private val directRoomHelper: DirectRoomHelper,
|
||||
private val callManager: WebRtcCallManager
|
||||
) {
|
||||
|
||||
class Failure : Throwable()
|
||||
|
||||
data class Result(val userId: String, val roomId: String)
|
||||
|
||||
suspend fun lookupPhoneNumber(phoneNumber: String): Result {
|
||||
val supportedProtocolKey = callManager.supportedPSTNProtocol ?: throw Failure()
|
||||
val thirdPartyUser = tryOrNull {
|
||||
session.thirdPartyService().getThirdPartyUser(supportedProtocolKey, fields = mapOf(
|
||||
"m.id.phone" to phoneNumber
|
||||
)).firstOrNull()
|
||||
session.thirdPartyService().getThirdPartyUser(
|
||||
protocol = supportedProtocolKey,
|
||||
fields = mapOf("m.id.phone" to phoneNumber)
|
||||
).firstOrNull()
|
||||
} ?: throw Failure()
|
||||
|
||||
val roomId = directRoomHelper.ensureDMExists(thirdPartyUser.userId)
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* 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.webrtc
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
|
||||
|
||||
private const val PSTN_VECTOR_KEY = "im.vector.protocol.pstn"
|
||||
private const val PSTN_MATRIX_KEY = "m.protocol.pstn"
|
||||
|
||||
suspend fun Session.getSupportedPSTN(maxTries: Int): String? {
|
||||
val thirdPartyProtocols: Map<String, ThirdPartyProtocol> = try {
|
||||
thirdPartyService().getThirdPartyProtocols()
|
||||
} catch (failure: Throwable) {
|
||||
if (maxTries == 1) {
|
||||
return null
|
||||
} else {
|
||||
// Wait for 10s before trying again
|
||||
delay(10_000L)
|
||||
return getSupportedPSTN(maxTries - 1)
|
||||
}
|
||||
}
|
||||
return when {
|
||||
thirdPartyProtocols.containsKey(PSTN_VECTOR_KEY) -> PSTN_VECTOR_KEY
|
||||
thirdPartyProtocols.containsKey(PSTN_MATRIX_KEY) -> PSTN_MATRIX_KEY
|
||||
else -> null
|
||||
}
|
||||
}
|
|
@ -53,7 +53,6 @@ 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.CallNegotiateContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.SdpType
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
import org.threeten.bp.Duration
|
||||
import org.webrtc.AudioSource
|
||||
import org.webrtc.AudioTrack
|
||||
|
@ -420,9 +419,7 @@ class WebRtcCall(val mxCall: MxCall,
|
|||
|
||||
private suspend fun getTurnServer(): TurnServerResponse? {
|
||||
return tryOrNull {
|
||||
awaitCallback {
|
||||
sessionProvider.get()?.callSignalingService()?.getTurnServer(it)
|
||||
}
|
||||
sessionProvider.get()?.callSignalingService()?.getTurnServer()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,14 +26,13 @@ import im.vector.app.features.call.VectorCallActivity
|
|||
import im.vector.app.features.call.audio.CallAudioManager
|
||||
import im.vector.app.features.call.utils.EglUtils
|
||||
import im.vector.app.push.fcm.FcmHelper
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.call.CallListener
|
||||
import org.matrix.android.sdk.api.session.call.CallState
|
||||
import org.matrix.android.sdk.api.session.call.MxCall
|
||||
import org.matrix.android.sdk.api.session.call.PSTNProtocolChecker
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
|
||||
|
@ -65,22 +64,26 @@ class WebRtcCallManager @Inject constructor(
|
|||
private val currentSession: Session?
|
||||
get() = activeSessionDataSource.currentValue?.orNull()
|
||||
|
||||
private val pstnProtocolChecker: PSTNProtocolChecker?
|
||||
get() = currentSession?.callSignalingService()?.getPSTNProtocolChecker()
|
||||
|
||||
interface CurrentCallListener {
|
||||
fun onCurrentCallChange(call: WebRtcCall?) {}
|
||||
fun onAudioDevicesChange() {}
|
||||
}
|
||||
|
||||
interface PSTNSupportListener {
|
||||
fun onPSTNSupportUpdated()
|
||||
val supportedPSTNProtocol: String?
|
||||
get() = pstnProtocolChecker?.supportedPSTNProtocol
|
||||
|
||||
val supportsPSTNProtocol: Boolean
|
||||
get() = supportedPSTNProtocol != null
|
||||
|
||||
fun addPstnSupportListener(listener: PSTNProtocolChecker.Listener) {
|
||||
pstnProtocolChecker?.addListener(listener)
|
||||
}
|
||||
|
||||
private val pstnSupportListeners = emptyList<PSTNSupportListener>().toMutableList()
|
||||
fun addPstnSupportListener(listener: PSTNSupportListener) {
|
||||
pstnSupportListeners.add(listener)
|
||||
}
|
||||
|
||||
fun removePstnSupportListener(listener: PSTNSupportListener) {
|
||||
pstnSupportListeners.remove(listener)
|
||||
fun removePstnSupportListener(listener: PSTNProtocolChecker.Listener) {
|
||||
pstnProtocolChecker?.removeListener(listener)
|
||||
}
|
||||
|
||||
private val currentCallsListeners = CopyOnWriteArrayList<CurrentCallListener>()
|
||||
|
@ -104,27 +107,11 @@ class WebRtcCallManager @Inject constructor(
|
|||
private var peerConnectionFactory: PeerConnectionFactory? = null
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
private val dispatcher = executor.asCoroutineDispatcher()
|
||||
var supportedPSTNProtocol: String? = null
|
||||
private set
|
||||
|
||||
val supportsPSTNProtocol: Boolean
|
||||
get() = supportedPSTNProtocol != null
|
||||
|
||||
private val rootEglBase by lazy { EglUtils.rootEglBase }
|
||||
|
||||
private var isInBackground: Boolean = true
|
||||
|
||||
init {
|
||||
GlobalScope.launch {
|
||||
supportedPSTNProtocol = currentSession?.getSupportedPSTN(3)
|
||||
if (supportedPSTNProtocol != null) {
|
||||
pstnSupportListeners.forEach {
|
||||
tryOrNull { it.onPSTNSupportUpdated() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
fun entersForeground() {
|
||||
isInBackground = false
|
||||
|
@ -167,6 +154,10 @@ class WebRtcCallManager @Inject constructor(
|
|||
return callsByCallId.values.toList()
|
||||
}
|
||||
|
||||
fun checkForPSTNSupportIfNeeded() {
|
||||
pstnProtocolChecker?.checkForPSTNSupportIfNeeded()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a set of all advertised call during the lifetime of the app.
|
||||
*/
|
||||
|
@ -176,7 +167,6 @@ class WebRtcCallManager @Inject constructor(
|
|||
Timber.v("## VOIP headSetButtonTapped")
|
||||
val call = getCurrentCall() ?: return
|
||||
if (call.mxCall.state is CallState.LocalRinging) {
|
||||
// accept call
|
||||
call.acceptIncomingCall()
|
||||
}
|
||||
if (call.mxCall.state is CallState.Connected) {
|
||||
|
|
|
@ -26,8 +26,8 @@ import com.airbnb.mvrx.ViewModelContext
|
|||
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||
import com.jakewharton.rxrelay2.PublishRelay
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
|
@ -64,6 +64,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
|
|||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.raw.RawService
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.call.PSTNProtocolChecker
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.LocalEcho
|
||||
|
@ -120,7 +121,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
private val directRoomHelper: DirectRoomHelper,
|
||||
timelineSettingsFactory: TimelineSettingsFactory
|
||||
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
|
||||
Timeline.Listener, ChatEffectManager.Delegate, WebRtcCallManager.PSTNSupportListener {
|
||||
Timeline.Listener, ChatEffectManager.Delegate, PSTNProtocolChecker.Listener {
|
||||
|
||||
private val room = session.getRoom(initialState.roomId)!!
|
||||
private val eventId = initialState.eventId
|
||||
|
@ -176,6 +177,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
// Inform the SDK that the room is displayed
|
||||
session.onRoomDisplayed(initialState.roomId)
|
||||
callManager.addPstnSupportListener(this)
|
||||
callManager.checkForPSTNSupportIfNeeded()
|
||||
chatEffectManager.delegate = this
|
||||
}
|
||||
|
||||
|
@ -231,65 +233,65 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
|
||||
override fun handle(action: RoomDetailAction) {
|
||||
when (action) {
|
||||
is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action)
|
||||
is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
|
||||
is RoomDetailAction.SendMessage -> handleSendMessage(action)
|
||||
is RoomDetailAction.SendMedia -> handleSendMedia(action)
|
||||
is RoomDetailAction.SendSticker -> handleSendSticker(action)
|
||||
is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action)
|
||||
is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action)
|
||||
is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action)
|
||||
is RoomDetailAction.SendReaction -> handleSendReaction(action)
|
||||
is RoomDetailAction.AcceptInvite -> handleAcceptInvite()
|
||||
is RoomDetailAction.RejectInvite -> handleRejectInvite()
|
||||
is RoomDetailAction.RedactAction -> handleRedactEvent(action)
|
||||
is RoomDetailAction.UndoReaction -> handleUndoReact(action)
|
||||
is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
|
||||
is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action)
|
||||
is RoomDetailAction.EnterEditMode -> handleEditAction(action)
|
||||
is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action)
|
||||
is RoomDetailAction.EnterReplyMode -> handleReplyAction(action)
|
||||
is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action)
|
||||
is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
|
||||
is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action)
|
||||
is RoomDetailAction.ResendMessage -> handleResendEvent(action)
|
||||
is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
|
||||
is RoomDetailAction.ResendAll -> handleResendAll()
|
||||
is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
|
||||
is RoomDetailAction.ReportContent -> handleReportContent(action)
|
||||
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
|
||||
is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action)
|
||||
is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
|
||||
is RoomDetailAction.SendMessage -> handleSendMessage(action)
|
||||
is RoomDetailAction.SendMedia -> handleSendMedia(action)
|
||||
is RoomDetailAction.SendSticker -> handleSendSticker(action)
|
||||
is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action)
|
||||
is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action)
|
||||
is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action)
|
||||
is RoomDetailAction.SendReaction -> handleSendReaction(action)
|
||||
is RoomDetailAction.AcceptInvite -> handleAcceptInvite()
|
||||
is RoomDetailAction.RejectInvite -> handleRejectInvite()
|
||||
is RoomDetailAction.RedactAction -> handleRedactEvent(action)
|
||||
is RoomDetailAction.UndoReaction -> handleUndoReact(action)
|
||||
is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
|
||||
is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action)
|
||||
is RoomDetailAction.EnterEditMode -> handleEditAction(action)
|
||||
is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action)
|
||||
is RoomDetailAction.EnterReplyMode -> handleReplyAction(action)
|
||||
is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action)
|
||||
is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
|
||||
is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action)
|
||||
is RoomDetailAction.ResendMessage -> handleResendEvent(action)
|
||||
is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
|
||||
is RoomDetailAction.ResendAll -> handleResendAll()
|
||||
is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
|
||||
is RoomDetailAction.ReportContent -> handleReportContent(action)
|
||||
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
|
||||
is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
|
||||
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
|
||||
is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action)
|
||||
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
|
||||
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
|
||||
is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
|
||||
is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action)
|
||||
is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action)
|
||||
is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action)
|
||||
is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
|
||||
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
|
||||
is RoomDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action)
|
||||
is RoomDetailAction.StartCall -> handleStartCall(action)
|
||||
is RoomDetailAction.AcceptCall -> handleAcceptCall(action)
|
||||
is RoomDetailAction.EndCall -> handleEndCall()
|
||||
is RoomDetailAction.ManageIntegrations -> handleManageIntegrations()
|
||||
is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action)
|
||||
is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId)
|
||||
is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action)
|
||||
is RoomDetailAction.CancelSend -> handleCancel(action)
|
||||
is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action)
|
||||
is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action)
|
||||
RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople()
|
||||
RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar()
|
||||
is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action)
|
||||
RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings)
|
||||
is RoomDetailAction.ShowRoomAvatarFullScreen -> {
|
||||
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
|
||||
is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action)
|
||||
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
|
||||
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
|
||||
is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
|
||||
is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action)
|
||||
is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action)
|
||||
is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action)
|
||||
is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
|
||||
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
|
||||
is RoomDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action)
|
||||
is RoomDetailAction.StartCall -> handleStartCall(action)
|
||||
is RoomDetailAction.AcceptCall -> handleAcceptCall(action)
|
||||
is RoomDetailAction.EndCall -> handleEndCall()
|
||||
is RoomDetailAction.ManageIntegrations -> handleManageIntegrations()
|
||||
is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action)
|
||||
is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId)
|
||||
is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action)
|
||||
is RoomDetailAction.CancelSend -> handleCancel(action)
|
||||
is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action)
|
||||
is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action)
|
||||
RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople()
|
||||
RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar()
|
||||
is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action)
|
||||
RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings)
|
||||
is RoomDetailAction.ShowRoomAvatarFullScreen -> {
|
||||
_viewEvents.post(
|
||||
RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView)
|
||||
)
|
||||
}
|
||||
is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action)
|
||||
is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action)
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
|
@ -618,10 +620,10 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
return@withState false
|
||||
}
|
||||
when (itemId) {
|
||||
R.id.resend_all -> state.asyncRoomSummary()?.hasFailedSending == true
|
||||
R.id.resend_all -> state.asyncRoomSummary()?.hasFailedSending == true
|
||||
R.id.timeline_setting -> true
|
||||
R.id.invite -> state.canInvite
|
||||
R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true
|
||||
R.id.invite -> state.canInvite
|
||||
R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true
|
||||
R.id.open_matrix_apps -> true
|
||||
R.id.voice_call,
|
||||
R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty()
|
||||
|
@ -741,7 +743,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.SendChatEffect -> {
|
||||
is ParsedCommand.SendChatEffect -> {
|
||||
sendChatEffect(slashCommandResult)
|
||||
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
|
||||
popDraft()
|
||||
|
@ -774,7 +776,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
}
|
||||
}.exhaustive
|
||||
}
|
||||
is SendMode.EDIT -> {
|
||||
is SendMode.EDIT -> {
|
||||
// is original event a reply?
|
||||
val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId
|
||||
if (inReplyTo != null) {
|
||||
|
@ -799,7 +801,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
||||
popDraft()
|
||||
}
|
||||
is SendMode.QUOTE -> {
|
||||
is SendMode.QUOTE -> {
|
||||
val messageContent: MessageContent? =
|
||||
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
|
||||
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
||||
|
@ -822,7 +824,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
||||
popDraft()
|
||||
}
|
||||
is SendMode.REPLY -> {
|
||||
is SendMode.REPLY -> {
|
||||
state.sendMode.timelineEvent.let {
|
||||
room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
|
||||
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
||||
|
@ -1441,7 +1443,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
override fun onPSTNSupportUpdated() {
|
||||
updateShowDialerOptionState()
|
||||
updateShowDialerOptionState()
|
||||
}
|
||||
|
||||
private fun updateShowDialerOptionState() {
|
||||
|
|
|
@ -19,7 +19,7 @@ package im.vector.app.features.home.room.detail
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import im.vector.app.core.platform.DefaultListUpdateCallback
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.item.BaseEventItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
|
@ -47,8 +47,8 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
|
|||
if (layoutManager.findFirstVisibleItemPosition() != position) {
|
||||
return
|
||||
}
|
||||
val firstNewItem = timelineEventController.adapter.getModelAtPosition(position) as? BaseEventItem ?: return
|
||||
val firstNewItemIds = firstNewItem.getEventIds().firstOrNull()
|
||||
val firstNewItem = timelineEventController.adapter.getModelAtPosition(position) as? ItemWithEvents ?: return
|
||||
val firstNewItemIds = firstNewItem.getEventIds().firstOrNull() ?: return
|
||||
val indexOfFirstNewItem = newTimelineEventIds.indexOf(firstNewItemIds)
|
||||
if (indexOfFirstNewItem != -1) {
|
||||
Timber.v("Should scroll to position: $position")
|
||||
|
|
|
@ -38,18 +38,15 @@ import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItem
|
|||
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory
|
||||
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.ReadMarkerVisibilityStateChangedListener
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineControllerInterceptorHelper
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.item.BaseEventItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
|
||||
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.MessageInformationData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import im.vector.app.features.media.VideoContentRenderer
|
||||
|
@ -194,75 +191,20 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
}
|
||||
}
|
||||
|
||||
private val interceptorHelper = TimelineControllerInterceptorHelper(
|
||||
::positionOfReadMarker,
|
||||
adapterPositionMapping,
|
||||
vectorPreferences,
|
||||
callManager
|
||||
)
|
||||
|
||||
init {
|
||||
addInterceptor(this)
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
// Update position when we are building new items
|
||||
override fun intercept(models: MutableList<EpoxyModel<*>>) = synchronized(modelCache) {
|
||||
positionOfReadMarker = null
|
||||
adapterPositionMapping.clear()
|
||||
val callIds = mutableSetOf<String>()
|
||||
val modelsIterator = models.listIterator()
|
||||
val showHiddenEvents = vectorPreferences.shouldShowHiddenEvents()
|
||||
modelsIterator.withIndex().forEach {
|
||||
val index = it.index
|
||||
val epoxyModel = it.value
|
||||
if (epoxyModel is CallTileTimelineItem) {
|
||||
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())
|
||||
if (shouldRemoveCallItem && !showHiddenEvents) {
|
||||
modelsIterator.remove()
|
||||
return@forEach
|
||||
}
|
||||
callIds.add(callId)
|
||||
}
|
||||
if (epoxyModel is BaseEventItem) {
|
||||
epoxyModel.getEventIds().forEach { eventId ->
|
||||
adapterPositionMapping[eventId] = index
|
||||
}
|
||||
}
|
||||
}
|
||||
val currentUnreadState = this.unreadState
|
||||
if (currentUnreadState is UnreadState.HasUnread) {
|
||||
val position = adapterPositionMapping[currentUnreadState.firstUnreadEventId]?.plus(1)
|
||||
positionOfReadMarker = position
|
||||
if (position != null) {
|
||||
val readMarker = TimelineReadMarkerItem_()
|
||||
.also {
|
||||
it.id("read_marker")
|
||||
it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback))
|
||||
}
|
||||
models.add(position, readMarker)
|
||||
}
|
||||
}
|
||||
val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false
|
||||
if (shouldAddBackwardPrefetch) {
|
||||
val indexOfPrefetchBackward = (previousModelsSize - 1)
|
||||
.coerceAtMost(models.size - DEFAULT_PREFETCH_THRESHOLD)
|
||||
.coerceAtLeast(0)
|
||||
|
||||
val loadingItem = LoadingItem_()
|
||||
.id("prefetch_backward_loading${System.currentTimeMillis()}")
|
||||
.showLoader(false)
|
||||
.setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS)
|
||||
|
||||
models.add(indexOfPrefetchBackward, loadingItem)
|
||||
}
|
||||
val shouldAddForwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.FORWARDS) ?: false
|
||||
if (shouldAddForwardPrefetch) {
|
||||
val indexOfPrefetchForward = DEFAULT_PREFETCH_THRESHOLD.coerceAtMost(models.size - 1)
|
||||
val loadingItem = LoadingItem_()
|
||||
.id("prefetch_forward_loading${System.currentTimeMillis()}")
|
||||
.showLoader(false)
|
||||
.setVisibilityStateChangedListener(Timeline.Direction.FORWARDS)
|
||||
models.add(indexOfPrefetchForward, loadingItem)
|
||||
}
|
||||
previousModelsSize = models.size
|
||||
interceptorHelper.intercept(models, unreadState, timeline, callback)
|
||||
}
|
||||
|
||||
fun update(viewState: RoomDetailViewState) {
|
||||
|
@ -431,6 +373,14 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
}
|
||||
}
|
||||
|
||||
private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ {
|
||||
return onVisibilityStateChanged { _, _, visibilityState ->
|
||||
if (visibilityState == VisibilityState.VISIBLE) {
|
||||
callback?.onLoadMore(direction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUTDStates(event: TimelineEvent, nextEvent: TimelineEvent?) {
|
||||
if (vectorPreferences.labShowCompleteHistoryInEncryptedRoom()) {
|
||||
return
|
||||
|
@ -461,14 +411,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
return shouldAdd
|
||||
}
|
||||
|
||||
private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ {
|
||||
return onVisibilityStateChanged { _, _, visibilityState ->
|
||||
if (visibilityState == VisibilityState.VISIBLE) {
|
||||
callback?.onLoadMore(direction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun searchPositionOfEvent(eventId: String?): Int? = synchronized(modelCache) {
|
||||
return adapterPositionMapping[eventId]
|
||||
}
|
||||
|
|
|
@ -16,7 +16,8 @@
|
|||
|
||||
package im.vector.app.features.home.room.detail.timeline.factory
|
||||
|
||||
import im.vector.app.core.epoxy.EmptyItem_
|
||||
import im.vector.app.core.epoxy.TimelineEmptyItem
|
||||
import im.vector.app.core.epoxy.TimelineEmptyItem_
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.resources.UserPreferencesProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
|
@ -114,6 +115,12 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
Timber.e(throwable, "failed to create message item")
|
||||
defaultItemFactory.create(event, highlight, callback, throwable)
|
||||
}
|
||||
return (computedModel ?: EmptyItem_())
|
||||
return computedModel ?: buildEmptyItem(event)
|
||||
}
|
||||
|
||||
private fun buildEmptyItem(timelineEvent: TimelineEvent): TimelineEmptyItem {
|
||||
return TimelineEmptyItem_()
|
||||
.id(timelineEvent.localId)
|
||||
.eventId(timelineEvent.eventId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* 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 com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.VisibilityState
|
||||
import im.vector.app.core.epoxy.LoadingItem_
|
||||
import im.vector.app.core.epoxy.TimelineEmptyItem_
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import im.vector.app.features.home.room.detail.UnreadState
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
|
||||
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import kotlin.reflect.KMutableProperty0
|
||||
|
||||
private const val DEFAULT_PREFETCH_THRESHOLD = 30
|
||||
|
||||
class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMutableProperty0<Int?>,
|
||||
private val adapterPositionMapping: MutableMap<String, Int>,
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val callManager: WebRtcCallManager
|
||||
) {
|
||||
|
||||
private var previousModelsSize = 0
|
||||
|
||||
// Update position when we are building new items
|
||||
fun intercept(
|
||||
models: MutableList<EpoxyModel<*>>,
|
||||
unreadState: UnreadState,
|
||||
timeline: Timeline?,
|
||||
callback: TimelineEventController.Callback?
|
||||
) {
|
||||
positionOfReadMarker.set(null)
|
||||
adapterPositionMapping.clear()
|
||||
val callIds = mutableSetOf<String>()
|
||||
|
||||
// Add some prefetch loader if needed
|
||||
models.addBackwardPrefetchIfNeeded(timeline, callback)
|
||||
models.addForwardPrefetchIfNeeded(timeline, callback)
|
||||
|
||||
val modelsIterator = models.listIterator()
|
||||
val showHiddenEvents = vectorPreferences.shouldShowHiddenEvents()
|
||||
var index = 0
|
||||
val firstUnreadEventId = (unreadState as? UnreadState.HasUnread)?.firstUnreadEventId
|
||||
// Then iterate on models so we have the exact positions in the adapter
|
||||
modelsIterator.forEach { epoxyModel ->
|
||||
if (epoxyModel is ItemWithEvents) {
|
||||
epoxyModel.getEventIds().forEach { eventId ->
|
||||
adapterPositionMapping[eventId] = index
|
||||
if (eventId == firstUnreadEventId) {
|
||||
modelsIterator.addReadMarkerItem(callback)
|
||||
index++
|
||||
positionOfReadMarker.set(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (epoxyModel is CallTileTimelineItem) {
|
||||
modelsIterator.removeCallItemIfNeeded(epoxyModel, callIds, showHiddenEvents)
|
||||
}
|
||||
index++
|
||||
}
|
||||
previousModelsSize = models.size
|
||||
}
|
||||
|
||||
private fun MutableListIterator<EpoxyModel<*>>.addReadMarkerItem(callback: TimelineEventController.Callback?) {
|
||||
val readMarker = TimelineReadMarkerItem_()
|
||||
.also {
|
||||
it.id("read_marker")
|
||||
it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback))
|
||||
}
|
||||
add(readMarker)
|
||||
// Use next as we still have some process to do before the next iterator loop
|
||||
next()
|
||||
}
|
||||
|
||||
private fun MutableListIterator<EpoxyModel<*>>.removeCallItemIfNeeded(
|
||||
epoxyModel: CallTileTimelineItem,
|
||||
callIds: MutableSet<String>,
|
||||
showHiddenEvents: 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())
|
||||
if (shouldRemoveCallItem && !showHiddenEvents) {
|
||||
remove()
|
||||
val emptyItem = TimelineEmptyItem_()
|
||||
.id(epoxyModel.id())
|
||||
.eventId(epoxyModel.attributes.informationData.eventId)
|
||||
add(emptyItem)
|
||||
}
|
||||
callIds.add(callId)
|
||||
}
|
||||
|
||||
private fun MutableList<EpoxyModel<*>>.addBackwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) {
|
||||
val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false
|
||||
if (shouldAddBackwardPrefetch) {
|
||||
val indexOfPrefetchBackward = (previousModelsSize - 1)
|
||||
.coerceAtMost(size - DEFAULT_PREFETCH_THRESHOLD)
|
||||
.coerceAtLeast(0)
|
||||
|
||||
val loadingItem = LoadingItem_()
|
||||
.id("prefetch_backward_loading${System.currentTimeMillis()}")
|
||||
.showLoader(false)
|
||||
.setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS, callback)
|
||||
|
||||
add(indexOfPrefetchBackward, loadingItem)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<EpoxyModel<*>>.addForwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) {
|
||||
val shouldAddForwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.FORWARDS) ?: false
|
||||
if (shouldAddForwardPrefetch) {
|
||||
val indexOfPrefetchForward = DEFAULT_PREFETCH_THRESHOLD.coerceAtMost(size - 1)
|
||||
val loadingItem = LoadingItem_()
|
||||
.id("prefetch_forward_loading${System.currentTimeMillis()}")
|
||||
.showLoader(false)
|
||||
.setVisibilityStateChangedListener(Timeline.Direction.FORWARDS, callback)
|
||||
add(indexOfPrefetchForward, loadingItem)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LoadingItem_.setVisibilityStateChangedListener(
|
||||
direction: Timeline.Direction,
|
||||
callback: TimelineEventController.Callback?
|
||||
): LoadingItem_ {
|
||||
return onVisibilityStateChanged { _, _, visibilityState ->
|
||||
if (visibilityState == VisibilityState.VISIBLE) {
|
||||
callback?.onLoadMore(direction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@ import im.vector.app.core.utils.DimensionConverter
|
|||
/**
|
||||
* Children must override getViewType()
|
||||
*/
|
||||
abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>() {
|
||||
abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>(), ItemWithEvents {
|
||||
|
||||
// To use for instance when opening a permalink with an eventId
|
||||
@EpoxyAttribute
|
||||
|
@ -53,12 +53,6 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
|
|||
holder.checkableBackground.isChecked = highlighted
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the eventIds associated with the EventItem.
|
||||
* Will generally get only one, but it handles the merging items.
|
||||
*/
|
||||
abstract fun getEventIds(): List<String>
|
||||
|
||||
abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() {
|
||||
val leftGuideline by bind<View>(R.id.messageStartGuideline)
|
||||
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
* 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.
|
||||
|
@ -14,12 +14,12 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.core.epoxy
|
||||
package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_empty)
|
||||
abstract class EmptyItem : VectorEpoxyModel<EmptyItem.Holder>() {
|
||||
class Holder : VectorEpoxyHolder()
|
||||
interface ItemWithEvents {
|
||||
/**
|
||||
* Returns the eventIds associated with the EventItem.
|
||||
* Will generally get only one, but it handles the merged items.
|
||||
*/
|
||||
fun getEventIds(): List<String>
|
||||
}
|
Loading…
Reference in New Issue