diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml
index 16cc35cebe6..4de90e94052 100644
--- a/.idea/dictionaries/bmarty.xml
+++ b/.idea/dictionaries/bmarty.xml
@@ -26,6 +26,7 @@
pkcs
previewable
previewables
+ pstn
riotx
signin
signout
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt
index c6bdcd19c77..dc67aa536af 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt
@@ -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): Cancelable
+ suspend fun getTurnServer(): TurnServerResponse
+
+ fun getPSTNProtocolChecker(): PSTNProtocolChecker
/**
* Create an outgoing call
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/PSTNProtocolChecker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/PSTNProtocolChecker.kt
new file mode 100644
index 00000000000..6627f62e24a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/PSTNProtocolChecker.kt
@@ -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()
+
+ 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 = 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
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt
index 10690c59dee..7d046cb6422 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt
@@ -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): 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 {
diff --git a/vector/src/main/java/im/vector/app/core/epoxy/TimelineEmptyItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/TimelineEmptyItem.kt
new file mode 100644
index 00000000000..b77670ba760
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/epoxy/TimelineEmptyItem.kt
@@ -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(), ItemWithEvents {
+
+ @EpoxyAttribute lateinit var eventId: String
+
+ override fun getEventIds(): List {
+ return listOf(eventId)
+ }
+
+ class Holder : VectorEpoxyHolder()
+}
diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt
index 25b2a80a854..8a2d56a5a25 100644
--- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt
@@ -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 {
- 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))
+ }
}
}
}
diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt
index 1c5caee2cd9..6fccea6c8c4 100644
--- a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt
+++ b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt
@@ -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)
diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/PSTNProtocol.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/PSTNProtocol.kt
deleted file mode 100644
index 3e6d2df6904..00000000000
--- a/vector/src/main/java/im/vector/app/features/call/webrtc/PSTNProtocol.kt
+++ /dev/null
@@ -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 = 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
- }
-}
diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt
index c72d7c8a76b..469fba4d5e0 100644
--- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt
+++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt
@@ -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()
}
}
diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt
index 95728e0a97c..2f8f84051e4 100644
--- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt
+++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt
@@ -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
- private val pstnSupportListeners = emptyList().toMutableList()
- fun addPstnSupportListener(listener: PSTNSupportListener) {
- pstnSupportListeners.add(listener)
+ fun addPstnSupportListener(listener: PSTNProtocolChecker.Listener) {
+ pstnProtocolChecker?.addListener(listener)
}
- fun removePstnSupportListener(listener: PSTNSupportListener) {
- pstnSupportListeners.remove(listener)
+ fun removePstnSupportListener(listener: PSTNProtocolChecker.Listener) {
+ pstnProtocolChecker?.removeListener(listener)
}
private val currentCallsListeners = CopyOnWriteArrayList()
@@ -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) {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
index 331be9d8258..c7a5873a659 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
@@ -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(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() {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt
index af56e2eb02e..fbf9ebe32fe 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt
@@ -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")
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
index 29871cf307c..9acd34c8275 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
@@ -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>) = synchronized(modelCache) {
- positionOfReadMarker = null
- adapterPositionMapping.clear()
- val callIds = mutableSetOf()
- 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]
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
index 982ceb906cd..7fd50147d43 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
@@ -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)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt
new file mode 100644
index 00000000000..971a3a35d83
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt
@@ -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,
+ private val adapterPositionMapping: MutableMap,
+ private val vectorPreferences: VectorPreferences,
+ private val callManager: WebRtcCallManager
+) {
+
+ private var previousModelsSize = 0
+
+ // Update position when we are building new items
+ fun intercept(
+ models: MutableList>,
+ unreadState: UnreadState,
+ timeline: Timeline?,
+ callback: TimelineEventController.Callback?
+ ) {
+ positionOfReadMarker.set(null)
+ adapterPositionMapping.clear()
+ val callIds = mutableSetOf()
+
+ // 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>.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>.removeCallItemIfNeeded(
+ epoxyModel: CallTileTimelineItem,
+ callIds: MutableSet,
+ 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>.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>.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)
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt
index e6174899022..13bb6db6efc 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt
@@ -32,7 +32,7 @@ import im.vector.app.core.utils.DimensionConverter
/**
* Children must override getViewType()
*/
-abstract class BaseEventItem : VectorEpoxyModel() {
+abstract class BaseEventItem : VectorEpoxyModel(), ItemWithEvents {
// To use for instance when opening a permalink with an eventId
@EpoxyAttribute
@@ -53,12 +53,6 @@ abstract class BaseEventItem : VectorEpoxyModel
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
-
abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() {
val leftGuideline by bind(R.id.messageStartGuideline)
val checkableBackground by bind(R.id.messageSelectedBackground)
diff --git a/vector/src/main/java/im/vector/app/core/epoxy/EmptyItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ItemWithEvents.kt
similarity index 65%
rename from vector/src/main/java/im/vector/app/core/epoxy/EmptyItem.kt
rename to vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ItemWithEvents.kt
index aaf870667b9..cf4211bb2cb 100644
--- a/vector/src/main/java/im/vector/app/core/epoxy/EmptyItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ItemWithEvents.kt
@@ -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() {
- 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
}
diff --git a/vector/src/main/res/layout/item_empty.xml b/vector/src/main/res/layout/item_timeline_empty.xml
similarity index 100%
rename from vector/src/main/res/layout/item_empty.xml
rename to vector/src/main/res/layout/item_timeline_empty.xml