Introduces CallEventGrouper so we can manage properly call history
This commit is contained in:
parent
d2785b69df
commit
f405532e4c
|
@ -41,6 +41,7 @@ import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItem
|
|||
import im.vector.app.features.home.room.detail.timeline.factory.ReadReceiptsItemFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.CallEventGrouper
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineControllerInterceptorHelper
|
||||
|
@ -48,7 +49,6 @@ import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiff
|
|||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_
|
||||
|
@ -56,7 +56,6 @@ import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
|
|||
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
|
||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import im.vector.app.features.media.VideoContentRenderer
|
||||
|
@ -164,6 +163,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
|
||||
// Map eventId to adapter position
|
||||
private val adapterPositionMapping = HashMap<String, Int>()
|
||||
private val callEventGroupers = HashMap<String, CallEventGrouper>()
|
||||
private val receiptsByEvent = HashMap<String, MutableList<ReadReceipt>>()
|
||||
private val modelCache = arrayListOf<CacheItemData?>()
|
||||
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
||||
private var inSubmitList: Boolean = false
|
||||
|
@ -353,8 +354,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
if (modelCache.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val receiptsByEvents = getReadReceiptsByShownEvent()
|
||||
val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(receiptsByEvents)
|
||||
preprocessReverseEvents()
|
||||
val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(receiptsByEvent)
|
||||
(0 until modelCache.size).forEach { position ->
|
||||
val event = currentSnapshot[position]
|
||||
val nextEvent = currentSnapshot.nextOrNull(position)
|
||||
|
@ -362,22 +363,28 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull {
|
||||
timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId)
|
||||
}
|
||||
val params = TimelineItemFactoryParams(
|
||||
event = event,
|
||||
prevEvent = prevEvent,
|
||||
nextEvent = nextEvent,
|
||||
nextDisplayableEvent = nextDisplayableEvent,
|
||||
partialState = partialState,
|
||||
lastSentEventIdWithoutReadReceipts = lastSentEventWithoutReadReceipts,
|
||||
callback = callback
|
||||
)
|
||||
// Should be build if not cached or if model should be refreshed
|
||||
if (modelCache[position] == null || modelCache[position]?.isCacheable == false) {
|
||||
val callEventGrouper = if (EventType.isCallEvent(event.root.getClearType())) {
|
||||
(event.root.getClearContent()?.get("call_id") as? String)?.let { callId -> callEventGroupers[callId] }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val params = TimelineItemFactoryParams(
|
||||
event = event,
|
||||
prevEvent = prevEvent,
|
||||
nextEvent = nextEvent,
|
||||
nextDisplayableEvent = nextDisplayableEvent,
|
||||
partialState = partialState,
|
||||
lastSentEventIdWithoutReadReceipts = lastSentEventWithoutReadReceipts,
|
||||
callback = callback,
|
||||
callEventGrouper = callEventGrouper
|
||||
)
|
||||
modelCache[position] = buildCacheItem(params)
|
||||
}
|
||||
val itemCachedData = modelCache[position] ?: return@forEach
|
||||
// Then update with additional models if needed
|
||||
modelCache[position] = itemCachedData.enrichWithModels(event, nextEvent, position, receiptsByEvents)
|
||||
modelCache[position] = itemCachedData.enrichWithModels(event, nextEvent, position, receiptsByEvent)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -450,15 +457,18 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
return null
|
||||
}
|
||||
|
||||
private fun getReadReceiptsByShownEvent(): Map<String, List<ReadReceipt>> {
|
||||
val receiptsByEvent = HashMap<String, MutableList<ReadReceipt>>()
|
||||
if (!userPreferencesProvider.shouldShowReadReceipts()) {
|
||||
return receiptsByEvent
|
||||
}
|
||||
var lastShownEventId: String? = null
|
||||
private fun preprocessReverseEvents() {
|
||||
receiptsByEvent.clear()
|
||||
callEventGroupers.clear()
|
||||
val itr = currentSnapshot.listIterator(currentSnapshot.size)
|
||||
var lastShownEventId: String? = null
|
||||
while (itr.hasPrevious()) {
|
||||
val event = itr.previous()
|
||||
if (EventType.isCallEvent(event.root.getClearType())) {
|
||||
(event.root.getClearContent()?.get("call_id") as? String)?.also { callId ->
|
||||
callEventGroupers.getOrPut(callId) { CallEventGrouper(session.myUserId, callId) }.add(event)
|
||||
}
|
||||
}
|
||||
val currentReadReceipts = ArrayList(event.readReceipts).filter {
|
||||
it.user.userId != session.myUserId
|
||||
}
|
||||
|
@ -471,7 +481,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
val existingReceipts = receiptsByEvent.getOrPut(lastShownEventId) { ArrayList() }
|
||||
existingReceipts.addAll(currentReadReceipts)
|
||||
}
|
||||
return receiptsByEvent
|
||||
}
|
||||
|
||||
private fun buildDaySeparatorItem(originServerTs: Long?): DaySeparatorItem {
|
||||
|
|
|
@ -16,81 +16,79 @@
|
|||
package im.vector.app.features.home.room.detail.timeline.factory
|
||||
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.resources.UserPreferencesProvider
|
||||
import im.vector.app.features.call.vectorCallService
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
|
||||
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class CallItemFactory @Inject constructor(
|
||||
private val session: Session,
|
||||
private val userPreferencesProvider: UserPreferencesProvider,
|
||||
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper,
|
||||
private val messageColorProvider: MessageColorProvider,
|
||||
private val messageInformationDataFactory: MessageInformationDataFactory,
|
||||
private val messageItemAttributesFactory: MessageItemAttributesFactory,
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val roomSummariesHolder: RoomSummariesHolder,
|
||||
private val callManager: WebRtcCallManager
|
||||
) {
|
||||
private val roomSummariesHolder: RoomSummariesHolder) {
|
||||
|
||||
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
|
||||
val event = params.event
|
||||
if (event.root.eventId == null) return null
|
||||
val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents()
|
||||
val callEventGrouper = params.callEventGrouper ?: return null
|
||||
val roomId = event.roomId
|
||||
val informationData = messageInformationDataFactory.create(params)
|
||||
val callSignalingContent = event.getCallSignalingContent() ?: return null
|
||||
val callId = callSignalingContent.callId ?: return null
|
||||
val call = callManager.getCallById(callId)
|
||||
val callKind = when {
|
||||
call == null -> CallTileTimelineItem.CallKind.UNKNOWN
|
||||
call.mxCall.isVideoCall -> CallTileTimelineItem.CallKind.VIDEO
|
||||
else -> CallTileTimelineItem.CallKind.AUDIO
|
||||
}
|
||||
val callKind = if (callEventGrouper.isVideo()) CallTileTimelineItem.CallKind.VIDEO else CallTileTimelineItem.CallKind.AUDIO
|
||||
val isRinging = callEventGrouper.isRinging()
|
||||
return when (event.root.getClearType()) {
|
||||
EventType.CALL_ANSWER -> {
|
||||
createCallTileTimelineItem(
|
||||
roomId = roomId,
|
||||
callId = callId,
|
||||
callStatus = CallTileTimelineItem.CallStatus.IN_CALL,
|
||||
callKind = callKind,
|
||||
callback = params.callback,
|
||||
highlight = params.isHighlighted,
|
||||
informationData = informationData,
|
||||
isStillActive = call != null
|
||||
)
|
||||
if (isRinging || showHiddenEvents) {
|
||||
createCallTileTimelineItem(
|
||||
roomId = roomId,
|
||||
callId = callEventGrouper.callId,
|
||||
callStatus = CallTileTimelineItem.CallStatus.IN_CALL,
|
||||
callKind = callKind,
|
||||
callback = params.callback,
|
||||
highlight = params.isHighlighted,
|
||||
informationData = informationData,
|
||||
isStillActive = isRinging
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
EventType.CALL_INVITE -> {
|
||||
createCallTileTimelineItem(
|
||||
roomId = roomId,
|
||||
callId = callId,
|
||||
callStatus = CallTileTimelineItem.CallStatus.INVITED,
|
||||
callKind = callKind,
|
||||
callback = params.callback,
|
||||
highlight = params.isHighlighted,
|
||||
informationData = informationData,
|
||||
isStillActive = call != null
|
||||
)
|
||||
if (isRinging || showHiddenEvents) {
|
||||
createCallTileTimelineItem(
|
||||
roomId = roomId,
|
||||
callId = callEventGrouper.callId,
|
||||
callStatus = CallTileTimelineItem.CallStatus.INVITED,
|
||||
callKind = callKind,
|
||||
callback = params.callback,
|
||||
highlight = params.isHighlighted,
|
||||
informationData = informationData,
|
||||
isStillActive = isRinging
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
EventType.CALL_REJECT -> {
|
||||
createCallTileTimelineItem(
|
||||
roomId = roomId,
|
||||
callId = callId,
|
||||
callId = callEventGrouper.callId,
|
||||
callStatus = CallTileTimelineItem.CallStatus.REJECTED,
|
||||
callKind = callKind,
|
||||
callback = params.callback,
|
||||
|
@ -102,8 +100,8 @@ class CallItemFactory @Inject constructor(
|
|||
EventType.CALL_HANGUP -> {
|
||||
createCallTileTimelineItem(
|
||||
roomId = roomId,
|
||||
callId = callId,
|
||||
callStatus = CallTileTimelineItem.CallStatus.ENDED,
|
||||
callId = callEventGrouper.callId,
|
||||
callStatus = if (callEventGrouper.callWasMissed()) CallTileTimelineItem.CallStatus.MISSED else CallTileTimelineItem.CallStatus.ENDED,
|
||||
callKind = callKind,
|
||||
callback = params.callback,
|
||||
highlight = params.isHighlighted,
|
||||
|
@ -115,16 +113,6 @@ class CallItemFactory @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun TimelineEvent.getCallSignalingContent(): CallSignalingContent? {
|
||||
return when (root.getClearType()) {
|
||||
EventType.CALL_INVITE -> root.getClearContent().toModel<CallInviteContent>()
|
||||
EventType.CALL_HANGUP -> root.getClearContent().toModel<CallHangupContent>()
|
||||
EventType.CALL_REJECT -> root.getClearContent().toModel<CallRejectContent>()
|
||||
EventType.CALL_ANSWER -> root.getClearContent().toModel<CallAnswerContent>()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCallTileTimelineItem(
|
||||
roomId: String,
|
||||
callId: String,
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.app.features.home.room.detail.timeline.factory
|
||||
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.CallEventGrouper
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
|
||||
data class TimelineItemFactoryParams(
|
||||
|
@ -26,7 +27,8 @@ data class TimelineItemFactoryParams(
|
|||
val nextDisplayableEvent: TimelineEvent? = null,
|
||||
val partialState: TimelineEventController.PartialState = TimelineEventController.PartialState(),
|
||||
val lastSentEventIdWithoutReadReceipts: String? = null,
|
||||
val callback: TimelineEventController.Callback? = null
|
||||
val callback: TimelineEventController.Callback? = null,
|
||||
val callEventGrouper: CallEventGrouper?= null
|
||||
) {
|
||||
|
||||
val highlightedEventId: String?
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
|
||||
class CallEventGrouper(private val myUserId: String, val callId: String) {
|
||||
|
||||
private val events = HashSet<TimelineEvent>()
|
||||
|
||||
fun add(timelineEvent: TimelineEvent) {
|
||||
events.add(timelineEvent)
|
||||
}
|
||||
|
||||
fun isVideo(): Boolean {
|
||||
val invite = getInvite() ?: return false
|
||||
return invite.root.getClearContent().toModel<CallInviteContent>()?.isVideo().orFalse()
|
||||
}
|
||||
|
||||
fun isRinging(): Boolean {
|
||||
return getAnswer() == null && getHangup() == null && getReject() == null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there are only events from the other side - we missed the call
|
||||
*/
|
||||
fun callWasMissed(): Boolean {
|
||||
return events.none { it.senderInfo.userId == myUserId }
|
||||
}
|
||||
|
||||
private fun getAnswer(): TimelineEvent? {
|
||||
return events.firstOrNull { it.root.getClearType() == EventType.CALL_ANSWER }
|
||||
}
|
||||
|
||||
private fun getInvite(): TimelineEvent? {
|
||||
return events.firstOrNull { it.root.getClearType() == EventType.CALL_INVITE }
|
||||
}
|
||||
|
||||
private fun getHangup(): TimelineEvent? {
|
||||
return events.firstOrNull { it.root.getClearType() == EventType.CALL_HANGUP }
|
||||
}
|
||||
|
||||
private fun getReject(): TimelineEvent? {
|
||||
return events.firstOrNull { it.root.getClearType() == EventType.CALL_REJECT }
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
|
@ -72,10 +73,22 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
|
|||
CallStatus.IN_CALL -> renderInCallStatus(holder)
|
||||
CallStatus.REJECTED -> renderRejectedStatus(holder)
|
||||
CallStatus.ENDED -> renderEndedStatus(holder)
|
||||
CallStatus.MISSED -> renderMissedStatus(holder)
|
||||
}
|
||||
renderSendState(holder.view, null, holder.failedToSendIndicator)
|
||||
}
|
||||
|
||||
private fun renderMissedStatus(holder: Holder) {
|
||||
holder.acceptRejectViewGroup.isVisible = false
|
||||
holder.statusView.isVisible = true
|
||||
val status = if (attributes.callKind == CallKind.VIDEO) {
|
||||
holder.resources.getQuantityString(R.plurals.missed_video_call, 1)
|
||||
} else {
|
||||
holder.resources.getQuantityString(R.plurals.missed_audio_call, 1)
|
||||
}
|
||||
holder.statusView.text = status
|
||||
}
|
||||
|
||||
private fun renderEndedStatus(holder: Holder) {
|
||||
holder.acceptRejectViewGroup.isVisible = false
|
||||
holder.statusView.isVisible = true
|
||||
|
@ -91,7 +104,7 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
|
|||
attributes.callback?.onTimelineItemAction(callbackAction)
|
||||
}
|
||||
} else {
|
||||
holder.statusView.text = holder.view.context.getString(R.string.call_tile_other_declined, attributes.userOfInterest.getBestName())
|
||||
holder.statusView.text = holder.resources.getString(R.string.call_tile_other_declined, attributes.userOfInterest.getBestName())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -166,7 +179,7 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
|
|||
if (attributes.informationData.sentByMe) {
|
||||
holder.statusView.setText(R.string.call_tile_you_started_call)
|
||||
} else {
|
||||
holder.statusView.text = holder.view.context.getString(R.string.call_tile_other_started_call, attributes.userOfInterest.getBestName())
|
||||
holder.statusView.text = holder.resources.getString(R.string.call_tile_other_started_call, attributes.userOfInterest.getBestName())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -182,6 +195,9 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
|
|||
val statusView by bind<TextView>(R.id.itemCallStatusTextView)
|
||||
val endGuideline by bind<View>(R.id.messageEndGuideline)
|
||||
val failedToSendIndicator by bind<ImageView>(R.id.messageFailToSendIndicator)
|
||||
|
||||
val resources: Resources
|
||||
get() = view.context.resources
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -215,6 +231,7 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
|
|||
INVITED,
|
||||
IN_CALL,
|
||||
REJECTED,
|
||||
MISSED,
|
||||
ENDED;
|
||||
|
||||
fun isActive() = this == INVITED || this == IN_CALL
|
||||
|
|
Loading…
Reference in New Issue